tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

dmg.py (8788B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import os
      6 import platform
      7 import shutil
      8 import subprocess
      9 from pathlib import Path
     10 
     11 import mozfile
     12 
     13 from mozbuild.dirutils import ensureParentDir
     14 
     15 is_linux = platform.system() == "Linux"
     16 is_osx = platform.system() == "Darwin"
     17 
     18 
     19 def chmod(dir):
     20    "Set permissions of DMG contents correctly"
     21    subprocess.check_call(["chmod", "-R", "a+rX,a-st,u+w,go-w", dir])
     22 
     23 
     24 def rsync(source: Path, dest: Path):
     25    "rsync the contents of directory source into directory dest"
     26    # Ensure a trailing slash on directories so rsync copies the *contents* of source.
     27    raw_source = str(source)
     28    if source.is_dir():
     29        raw_source = str(source) + "/"
     30    subprocess.check_call(["rsync", "-a", "--copy-unsafe-links", raw_source, dest])
     31 
     32 
     33 def set_folder_icon(dir: Path, tmpdir: Path, hfs_tool: Path = None):
     34    "Set HFS attributes of dir to use a custom icon"
     35    if is_linux:
     36        hfs = tmpdir / "staged.hfs"
     37        subprocess.check_call([hfs_tool, hfs, "attr", "/", "C"])
     38    elif is_osx:
     39        subprocess.check_call(["SetFile", "-a", "C", dir])
     40 
     41 
     42 def generate_hfs_file(
     43    stagedir: Path, tmpdir: Path, volume_name: str, mkfshfs_tool: Path
     44 ):
     45    """
     46    When cross compiling, we zero fill an hfs file, that we will turn into
     47    a DMG. To do so we test the size of the staged dir, and add some slight
     48    padding to that.
     49    """
     50    hfs = tmpdir / "staged.hfs"
     51    output = subprocess.check_output(["du", "-s", stagedir])
     52    size = int(output.split()[0]) / 1000  # Get in MB
     53    size = int(size * 1.02)  # Bump the used size slightly larger.
     54    # Setup a proper file sized out with zero's
     55    subprocess.check_call([
     56        "dd",
     57        "if=/dev/zero",
     58        f"of={hfs}",
     59        "bs=1M",
     60        f"count={size}",
     61    ])
     62    subprocess.check_call([mkfshfs_tool, "-v", volume_name, hfs])
     63 
     64 
     65 def create_app_symlink(stagedir: Path, tmpdir: Path, hfs_tool: Path = None):
     66    """
     67    Make a symlink to /Applications. The symlink name is a space
     68    so we don't have to localize it. The Applications folder icon
     69    will be shown in Finder, which should be clear enough for users.
     70    """
     71    if is_linux:
     72        hfs = os.path.join(tmpdir, "staged.hfs")
     73        subprocess.check_call([hfs_tool, hfs, "symlink", "/ ", "/Applications"])
     74    elif is_osx:
     75        os.symlink("/Applications", stagedir / " ")
     76 
     77 
     78 def create_dmg_from_staged(
     79    stagedir: Path,
     80    output_dmg: Path,
     81    tmpdir: Path,
     82    volume_name: str,
     83    hfs_tool: Path = None,
     84    dmg_tool: Path = None,
     85    mkfshfs_tool: Path = None,
     86    attribution_sentinel: str = None,
     87    compression: str = None,
     88 ):
     89    "Given a prepared directory stagedir, produce a DMG at output_dmg."
     90    if compression is None:
     91        # Easier to put the default here once, than in every place that takes default args
     92        compression = "bzip2"
     93    if compression not in ["bzip2", "lzma"]:
     94        raise Exception("Don't know how to handle %s compression" % (compression,))
     95 
     96    if is_linux:
     97        # The dmg tool doesn't create the destination directories, and silently
     98        # returns success if the parent directory doesn't exist.
     99        ensureParentDir(output_dmg)
    100        hfs = os.path.join(tmpdir, "staged.hfs")
    101        subprocess.check_call([hfs_tool, hfs, "addall", stagedir])
    102 
    103        dmg_cmd = [dmg_tool, "build", hfs, output_dmg]
    104        if attribution_sentinel:
    105            while len(attribution_sentinel) < 1024:
    106                attribution_sentinel += "\t"
    107            subprocess.check_call([
    108                hfs_tool,
    109                hfs,
    110                "setattr",
    111                f"{volume_name}.app",
    112                "com.apple.application-instance",
    113                attribution_sentinel,
    114            ])
    115            subprocess.check_call(["cp", hfs, str(Path(output_dmg).parent)])
    116            dmg_cmd.append(attribution_sentinel)
    117 
    118        if compression == "lzma":
    119            dmg_cmd.extend([
    120                "--compression",
    121                "lzma",
    122                "--level",
    123                "5",
    124                "--run-sectors",
    125                "2048",
    126            ])
    127 
    128        subprocess.check_call(
    129            dmg_cmd,
    130            # dmg is seriously chatty
    131            stdout=subprocess.DEVNULL,
    132        )
    133    elif is_osx:
    134        format = "UDBZ"
    135        if compression == "lzma":
    136            format = "ULMO"
    137 
    138        hybrid = tmpdir / "hybrid.dmg"
    139        subprocess.check_call([
    140            "hdiutil",
    141            "makehybrid",
    142            "-hfs",
    143            "-hfs-volume-name",
    144            volume_name,
    145            "-hfs-openfolder",
    146            stagedir,
    147            "-ov",
    148            stagedir,
    149            "-o",
    150            hybrid,
    151        ])
    152        subprocess.check_call([
    153            "hdiutil",
    154            "convert",
    155            "-format",
    156            format,
    157            "-imagekey",
    158            "bzip2-level=9",
    159            "-ov",
    160            hybrid,
    161            "-o",
    162            output_dmg,
    163        ])
    164 
    165 
    166 def create_dmg(
    167    source_directory: Path,
    168    output_dmg: Path,
    169    volume_name: str,
    170    extra_files: list[tuple],
    171    dmg_tool: Path,
    172    hfs_tool: Path,
    173    mkfshfs_tool: Path,
    174    attribution_sentinel: str = None,
    175    compression: str = None,
    176 ):
    177    """
    178    Create a DMG disk image at the path output_dmg from source_directory.
    179 
    180    Use volume_name as the disk image volume name, and
    181    use extra_files as a list of tuples of (filename, relative path) to copy
    182    into the disk image.
    183    """
    184    if platform.system() not in ("Darwin", "Linux"):
    185        raise Exception("Don't know how to build a DMG on '%s'" % platform.system())
    186 
    187    with mozfile.TemporaryDirectory() as tmp:
    188        tmpdir = Path(tmp)
    189        stagedir = tmpdir / "stage"
    190        stagedir.mkdir()
    191 
    192        # Copy the app bundle over using rsync
    193        rsync(source_directory, stagedir)
    194        # Copy extra files
    195        for source, target in extra_files:
    196            full_target = stagedir / target
    197            full_target.parent.mkdir(parents=True, exist_ok=True)
    198            shutil.copyfile(source, full_target)
    199        if is_linux:
    200            # Not needed in osx
    201            generate_hfs_file(stagedir, tmpdir, volume_name, mkfshfs_tool)
    202        create_app_symlink(stagedir, tmpdir, hfs_tool)
    203        # Set the folder attributes to use a custom icon
    204        set_folder_icon(stagedir, tmpdir, hfs_tool)
    205        chmod(stagedir)
    206        create_dmg_from_staged(
    207            stagedir,
    208            output_dmg,
    209            tmpdir,
    210            volume_name,
    211            hfs_tool,
    212            dmg_tool,
    213            mkfshfs_tool,
    214            attribution_sentinel,
    215            compression,
    216        )
    217 
    218 
    219 def extract_dmg_contents(
    220    dmgfile: Path,
    221    destdir: Path,
    222    dmg_tool: Path = None,
    223    hfs_tool: Path = None,
    224 ):
    225    if is_linux:
    226        with mozfile.TemporaryDirectory() as tmpdir:
    227            hfs_file = os.path.join(tmpdir, "firefox.hfs")
    228            subprocess.check_call(
    229                [dmg_tool, "extract", dmgfile, hfs_file],
    230                # dmg is seriously chatty
    231                stdout=subprocess.DEVNULL,
    232            )
    233            subprocess.check_call([hfs_tool, hfs_file, "extractall", "/", destdir])
    234    else:
    235        # TODO: find better way to resolve topsrcdir (checkout directory)
    236        topsrcdir = Path(__file__).parent.parent.parent.parent.resolve()
    237        unpack_diskimage = topsrcdir / "build/package/mac_osx/unpack-diskimage"
    238        unpack_mountpoint = Path("/tmp/app-unpack")
    239        subprocess.check_call([unpack_diskimage, dmgfile, unpack_mountpoint, destdir])
    240 
    241 
    242 def extract_dmg(
    243    dmgfile: Path,
    244    output: Path,
    245    dmg_tool: Path = None,
    246    hfs_tool: Path = None,
    247    dsstore: Path = None,
    248    icon: Path = None,
    249    background: Path = None,
    250 ):
    251    if platform.system() not in ("Darwin", "Linux"):
    252        raise Exception("Don't know how to extract a DMG on '%s'" % platform.system())
    253 
    254    with mozfile.TemporaryDirectory() as tmp:
    255        tmpdir = Path(tmp)
    256        extract_dmg_contents(dmgfile, tmpdir, dmg_tool, hfs_tool)
    257        applications_symlink = tmpdir / " "
    258        if applications_symlink.is_symlink():
    259            # Rsync will fail on the presence of this symlink
    260            applications_symlink.unlink()
    261        rsync(tmpdir, output)
    262 
    263        if dsstore:
    264            dsstore.parent.mkdir(parents=True, exist_ok=True)
    265            rsync(tmpdir / ".DS_Store", dsstore)
    266        if background:
    267            background.parent.mkdir(parents=True, exist_ok=True)
    268            rsync(tmpdir / ".background" / background.name, background)
    269        if icon:
    270            icon.parent.mkdir(parents=True, exist_ok=True)
    271            rsync(tmpdir / ".VolumeIcon.icns", icon)