275 lines
8.8 KiB
Python
275 lines
8.8 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import mozfile
|
|
|
|
from mozbuild.dirutils import ensureParentDir
|
|
|
|
is_linux = platform.system() == "Linux"
|
|
is_osx = platform.system() == "Darwin"
|
|
|
|
|
|
def chmod(dir):
|
|
"Set permissions of DMG contents correctly"
|
|
subprocess.check_call(["chmod", "-R", "a+rX,a-st,u+w,go-w", dir])
|
|
|
|
|
|
def rsync(source: Path, dest: Path):
|
|
"rsync the contents of directory source into directory dest"
|
|
# Ensure a trailing slash on directories so rsync copies the *contents* of source.
|
|
raw_source = str(source)
|
|
if source.is_dir():
|
|
raw_source = str(source) + "/"
|
|
subprocess.check_call(["rsync", "-a", "--copy-unsafe-links", raw_source, dest])
|
|
|
|
|
|
def set_folder_icon(dir: Path, tmpdir: Path, hfs_tool: Path = None):
|
|
"Set HFS attributes of dir to use a custom icon"
|
|
if is_linux:
|
|
hfs = tmpdir / "staged.hfs"
|
|
subprocess.check_call([hfs_tool, hfs, "attr", "/", "C"])
|
|
elif is_osx:
|
|
subprocess.check_call(["SetFile", "-a", "C", dir])
|
|
|
|
|
|
def generate_hfs_file(
|
|
stagedir: Path, tmpdir: Path, volume_name: str, mkfshfs_tool: Path
|
|
):
|
|
"""
|
|
When cross compiling, we zero fill an hfs file, that we will turn into
|
|
a DMG. To do so we test the size of the staged dir, and add some slight
|
|
padding to that.
|
|
"""
|
|
hfs = tmpdir / "staged.hfs"
|
|
output = subprocess.check_output(["du", "-s", stagedir])
|
|
size = int(output.split()[0]) / 1000 # Get in MB
|
|
size = int(size * 1.02) # Bump the used size slightly larger.
|
|
# Setup a proper file sized out with zero's
|
|
subprocess.check_call(
|
|
[
|
|
"dd",
|
|
"if=/dev/zero",
|
|
f"of={hfs}",
|
|
"bs=1M",
|
|
f"count={size}",
|
|
]
|
|
)
|
|
subprocess.check_call([mkfshfs_tool, "-v", volume_name, hfs])
|
|
|
|
|
|
def create_app_symlink(stagedir: Path, tmpdir: Path, hfs_tool: Path = None):
|
|
"""
|
|
Make a symlink to /Applications. The symlink name is a space
|
|
so we don't have to localize it. The Applications folder icon
|
|
will be shown in Finder, which should be clear enough for users.
|
|
"""
|
|
if is_linux:
|
|
hfs = os.path.join(tmpdir, "staged.hfs")
|
|
subprocess.check_call([hfs_tool, hfs, "symlink", "/ ", "/Applications"])
|
|
elif is_osx:
|
|
os.symlink("/Applications", stagedir / " ")
|
|
|
|
|
|
def create_dmg_from_staged(
|
|
stagedir: Path,
|
|
output_dmg: Path,
|
|
tmpdir: Path,
|
|
volume_name: str,
|
|
hfs_tool: Path = None,
|
|
dmg_tool: Path = None,
|
|
mkfshfs_tool: Path = None,
|
|
attribution_sentinel: str = None,
|
|
compression: str = None,
|
|
):
|
|
"Given a prepared directory stagedir, produce a DMG at output_dmg."
|
|
if compression is None:
|
|
# Easier to put the default here once, than in every place that takes default args
|
|
compression = "bzip2"
|
|
if compression not in ["bzip2", "lzma"]:
|
|
raise Exception("Don't know how to handle %s compression" % (compression,))
|
|
|
|
if is_linux:
|
|
# The dmg tool doesn't create the destination directories, and silently
|
|
# returns success if the parent directory doesn't exist.
|
|
ensureParentDir(output_dmg)
|
|
hfs = os.path.join(tmpdir, "staged.hfs")
|
|
subprocess.check_call([hfs_tool, hfs, "addall", stagedir])
|
|
|
|
dmg_cmd = [dmg_tool, "build", hfs, output_dmg]
|
|
if attribution_sentinel:
|
|
while len(attribution_sentinel) < 1024:
|
|
attribution_sentinel += "\t"
|
|
subprocess.check_call(
|
|
[
|
|
hfs_tool,
|
|
hfs,
|
|
"setattr",
|
|
f"{volume_name}.app",
|
|
"com.apple.application-instance",
|
|
attribution_sentinel,
|
|
]
|
|
)
|
|
subprocess.check_call(["cp", hfs, str(Path(output_dmg).parent)])
|
|
dmg_cmd.append(attribution_sentinel)
|
|
|
|
if compression == "lzma":
|
|
dmg_cmd.extend(
|
|
["--compression", "lzma", "--level", "5", "--run-sectors", "2048"]
|
|
)
|
|
|
|
subprocess.check_call(
|
|
dmg_cmd,
|
|
# dmg is seriously chatty
|
|
stdout=subprocess.DEVNULL,
|
|
)
|
|
elif is_osx:
|
|
format = "UDBZ"
|
|
if compression == "lzma":
|
|
format = "ULMO"
|
|
|
|
hybrid = tmpdir / "hybrid.dmg"
|
|
subprocess.check_call(
|
|
[
|
|
"hdiutil",
|
|
"makehybrid",
|
|
"-hfs",
|
|
"-hfs-volume-name",
|
|
volume_name,
|
|
"-hfs-openfolder",
|
|
stagedir,
|
|
"-ov",
|
|
stagedir,
|
|
"-o",
|
|
hybrid,
|
|
]
|
|
)
|
|
subprocess.check_call(
|
|
[
|
|
"hdiutil",
|
|
"convert",
|
|
"-format",
|
|
format,
|
|
"-imagekey",
|
|
"bzip2-level=9",
|
|
"-ov",
|
|
hybrid,
|
|
"-o",
|
|
output_dmg,
|
|
]
|
|
)
|
|
|
|
|
|
def create_dmg(
|
|
source_directory: Path,
|
|
output_dmg: Path,
|
|
volume_name: str,
|
|
extra_files: List[tuple],
|
|
dmg_tool: Path,
|
|
hfs_tool: Path,
|
|
mkfshfs_tool: Path,
|
|
attribution_sentinel: str = None,
|
|
compression: str = None,
|
|
):
|
|
"""
|
|
Create a DMG disk image at the path output_dmg from source_directory.
|
|
|
|
Use volume_name as the disk image volume name, and
|
|
use extra_files as a list of tuples of (filename, relative path) to copy
|
|
into the disk image.
|
|
"""
|
|
if platform.system() not in ("Darwin", "Linux"):
|
|
raise Exception("Don't know how to build a DMG on '%s'" % platform.system())
|
|
|
|
with mozfile.TemporaryDirectory() as tmp:
|
|
tmpdir = Path(tmp)
|
|
stagedir = tmpdir / "stage"
|
|
stagedir.mkdir()
|
|
|
|
# Copy the app bundle over using rsync
|
|
rsync(source_directory, stagedir)
|
|
# Copy extra files
|
|
for source, target in extra_files:
|
|
full_target = stagedir / target
|
|
full_target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(source, full_target)
|
|
if is_linux:
|
|
# Not needed in osx
|
|
generate_hfs_file(stagedir, tmpdir, volume_name, mkfshfs_tool)
|
|
create_app_symlink(stagedir, tmpdir, hfs_tool)
|
|
# Set the folder attributes to use a custom icon
|
|
set_folder_icon(stagedir, tmpdir, hfs_tool)
|
|
chmod(stagedir)
|
|
create_dmg_from_staged(
|
|
stagedir,
|
|
output_dmg,
|
|
tmpdir,
|
|
volume_name,
|
|
hfs_tool,
|
|
dmg_tool,
|
|
mkfshfs_tool,
|
|
attribution_sentinel,
|
|
compression,
|
|
)
|
|
|
|
|
|
def extract_dmg_contents(
|
|
dmgfile: Path,
|
|
destdir: Path,
|
|
dmg_tool: Path = None,
|
|
hfs_tool: Path = None,
|
|
):
|
|
if is_linux:
|
|
with mozfile.TemporaryDirectory() as tmpdir:
|
|
hfs_file = os.path.join(tmpdir, "firefox.hfs")
|
|
subprocess.check_call(
|
|
[dmg_tool, "extract", dmgfile, hfs_file],
|
|
# dmg is seriously chatty
|
|
stdout=subprocess.DEVNULL,
|
|
)
|
|
subprocess.check_call([hfs_tool, hfs_file, "extractall", "/", destdir])
|
|
else:
|
|
# TODO: find better way to resolve topsrcdir (checkout directory)
|
|
topsrcdir = Path(__file__).parent.parent.parent.parent.resolve()
|
|
unpack_diskimage = topsrcdir / "build/package/mac_osx/unpack-diskimage"
|
|
unpack_mountpoint = Path("/tmp/app-unpack")
|
|
subprocess.check_call([unpack_diskimage, dmgfile, unpack_mountpoint, destdir])
|
|
|
|
|
|
def extract_dmg(
|
|
dmgfile: Path,
|
|
output: Path,
|
|
dmg_tool: Path = None,
|
|
hfs_tool: Path = None,
|
|
dsstore: Path = None,
|
|
icon: Path = None,
|
|
background: Path = None,
|
|
):
|
|
if platform.system() not in ("Darwin", "Linux"):
|
|
raise Exception("Don't know how to extract a DMG on '%s'" % platform.system())
|
|
|
|
with mozfile.TemporaryDirectory() as tmp:
|
|
tmpdir = Path(tmp)
|
|
extract_dmg_contents(dmgfile, tmpdir, dmg_tool, hfs_tool)
|
|
applications_symlink = tmpdir / " "
|
|
if applications_symlink.is_symlink():
|
|
# Rsync will fail on the presence of this symlink
|
|
applications_symlink.unlink()
|
|
rsync(tmpdir, output)
|
|
|
|
if dsstore:
|
|
dsstore.parent.mkdir(parents=True, exist_ok=True)
|
|
rsync(tmpdir / ".DS_Store", dsstore)
|
|
if background:
|
|
background.parent.mkdir(parents=True, exist_ok=True)
|
|
rsync(tmpdir / ".background" / background.name, background)
|
|
if icon:
|
|
icon.parent.mkdir(parents=True, exist_ok=True)
|
|
rsync(tmpdir / ".VolumeIcon.icns", icon)
|