mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	Switches from `buildconfig.substs` to `bootstrap_toolchain` for the repackage job
Refactors mozpack/dmg to not depend on the environment; Moves path resolution of tools up to `mozbuild.action.{make_dmg|unpack_dmg}`
Update syntax to python 3.7+
Differential Revision: https://phabricator.services.mozilla.com/D163129
		
	
			
		
			
				
	
	
		
			299 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
	
		
			9.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 concurrent.futures
 | 
						|
import lzma
 | 
						|
import os
 | 
						|
import plistlib
 | 
						|
import struct
 | 
						|
import subprocess
 | 
						|
from pathlib import Path
 | 
						|
from string import Template
 | 
						|
from typing import List
 | 
						|
from urllib.parse import quote
 | 
						|
 | 
						|
import mozfile
 | 
						|
 | 
						|
TEMPLATE_DIRECTORY = Path(__file__).parent / "apple_pkg"
 | 
						|
PBZX_CHUNK_SIZE = 16 * 1024 * 1024  # 16MB chunks
 | 
						|
 | 
						|
 | 
						|
def get_apple_template(name: str) -> Template:
 | 
						|
    """
 | 
						|
    Given <name>, open file at <TEMPLATE_DIRECTORY>/<name>, read contents and
 | 
						|
        return as a Template
 | 
						|
 | 
						|
    Args:
 | 
						|
        name: str, Filename for the template
 | 
						|
 | 
						|
    Returns:
 | 
						|
        Template, loaded from file
 | 
						|
    """
 | 
						|
    tmpl_path = TEMPLATE_DIRECTORY / name
 | 
						|
    if not tmpl_path.is_file():
 | 
						|
        raise Exception(f"Could not find template: {tmpl_path}")
 | 
						|
    with tmpl_path.open("r") as tmpl:
 | 
						|
        contents = tmpl.read()
 | 
						|
    return Template(contents)
 | 
						|
 | 
						|
 | 
						|
def save_text_file(content: str, destination: Path):
 | 
						|
    """
 | 
						|
    Saves a text file to <destination> with provided <content>
 | 
						|
    Note: Overwrites contents
 | 
						|
 | 
						|
    Args:
 | 
						|
        content: str, The desired contents of the file
 | 
						|
        destination: Path, The file path
 | 
						|
    """
 | 
						|
    with destination.open("w") as out_fd:
 | 
						|
        out_fd.write(content)
 | 
						|
    print(f"Created text file at {destination}")
 | 
						|
    print(f"Created text file size: {destination.stat().st_size} bytes")
 | 
						|
 | 
						|
 | 
						|
def get_app_info_plist(app_path: Path) -> dict:
 | 
						|
    """
 | 
						|
    Retrieve most information from Info.plist file of an app.
 | 
						|
    The Info.plist file should be located in ?.app/Contents/Info.plist
 | 
						|
 | 
						|
    Note: Ignores properties that are not <string> type
 | 
						|
 | 
						|
    Args:
 | 
						|
        app_path: Path, the .app file/directory path
 | 
						|
 | 
						|
    Returns:
 | 
						|
        dict, the dictionary of properties found in Info.plist
 | 
						|
    """
 | 
						|
    info_plist = app_path / "Contents/Info.plist"
 | 
						|
    if not info_plist.is_file():
 | 
						|
        raise Exception(f"Could not find Info.plist in {info_plist}")
 | 
						|
 | 
						|
    print(f"Reading app Info.plist from: {info_plist}")
 | 
						|
 | 
						|
    with info_plist.open("rb") as plist_fd:
 | 
						|
        data = plistlib.load(plist_fd)
 | 
						|
 | 
						|
    return data
 | 
						|
 | 
						|
 | 
						|
def create_payload(destination: Path, root_path: Path, cpio_tool: str):
 | 
						|
    """
 | 
						|
    Creates a payload at <destination> based on <root_path>
 | 
						|
 | 
						|
    Args:
 | 
						|
        destination: Path, the destination Path
 | 
						|
        root_path: Path, the root directory Path
 | 
						|
        cpio_tool: str,
 | 
						|
    """
 | 
						|
    # Files to be cpio'd are root folder + contents
 | 
						|
    file_list = ["./"] + get_relative_glob_list(root_path, "**/*")
 | 
						|
 | 
						|
    with mozfile.TemporaryDirectory() as tmp_dir:
 | 
						|
        tmp_payload_path = Path(tmp_dir) / "Payload"
 | 
						|
        print(f"Creating Payload with cpio from {root_path} to {tmp_payload_path}")
 | 
						|
        print(f"Found {len(file_list)} files")
 | 
						|
        with tmp_payload_path.open("wb") as tmp_payload:
 | 
						|
            process = subprocess.run(
 | 
						|
                [
 | 
						|
                    cpio_tool,
 | 
						|
                    "-o",  # copy-out mode
 | 
						|
                    "--format",
 | 
						|
                    "odc",  # old POSIX .1 portable format
 | 
						|
                    "--owner",
 | 
						|
                    "0:80",  # clean ownership
 | 
						|
                ],
 | 
						|
                stdout=tmp_payload,
 | 
						|
                stderr=subprocess.PIPE,
 | 
						|
                input="\n".join(file_list) + "\n",
 | 
						|
                encoding="ascii",
 | 
						|
                cwd=root_path,
 | 
						|
            )
 | 
						|
        # cpio outputs number of blocks to stderr
 | 
						|
        print(f"[CPIO]: {process.stderr}")
 | 
						|
        if process.returncode:
 | 
						|
            raise Exception(f"CPIO error {process.returncode}")
 | 
						|
 | 
						|
        tmp_payload_size = tmp_payload_path.stat().st_size
 | 
						|
        print(f"Uncompressed Payload size: {tmp_payload_size // 1024}kb")
 | 
						|
 | 
						|
        def compress_chunk(chunk):
 | 
						|
            compressed_chunk = lzma.compress(chunk)
 | 
						|
            return len(chunk), compressed_chunk
 | 
						|
 | 
						|
        def chunker(fileobj, chunk_size):
 | 
						|
            while True:
 | 
						|
                chunk = fileobj.read(chunk_size)
 | 
						|
                if not chunk:
 | 
						|
                    break
 | 
						|
                yield chunk
 | 
						|
 | 
						|
        with tmp_payload_path.open("rb") as f_in, destination.open(
 | 
						|
            "wb"
 | 
						|
        ) as f_out, concurrent.futures.ThreadPoolExecutor(
 | 
						|
            max_workers=os.cpu_count()
 | 
						|
        ) as executor:
 | 
						|
            f_out.write(b"pbzx")
 | 
						|
            f_out.write(struct.pack(">Q", PBZX_CHUNK_SIZE))
 | 
						|
            chunks = chunker(f_in, PBZX_CHUNK_SIZE)
 | 
						|
            for uncompressed_size, compressed_chunk in executor.map(
 | 
						|
                compress_chunk, chunks
 | 
						|
            ):
 | 
						|
                f_out.write(struct.pack(">Q", uncompressed_size))
 | 
						|
                if len(compressed_chunk) < uncompressed_size:
 | 
						|
                    f_out.write(struct.pack(">Q", len(compressed_chunk)))
 | 
						|
                    f_out.write(compressed_chunk)
 | 
						|
                else:
 | 
						|
                    # Considering how unlikely this is, we prefer to just decompress
 | 
						|
                    # here than to keep the original uncompressed chunk around
 | 
						|
                    f_out.write(struct.pack(">Q", uncompressed_size))
 | 
						|
                    f_out.write(lzma.decompress(compressed_chunk))
 | 
						|
 | 
						|
        print(f"Compressed Payload file to {destination}")
 | 
						|
        print(f"Compressed Payload size: {destination.stat().st_size // 1024}kb")
 | 
						|
 | 
						|
 | 
						|
def create_bom(bom_path: Path, root_path: Path, mkbom_tool: Path):
 | 
						|
    """
 | 
						|
    Creates a Bill Of Materials file at <bom_path> based on <root_path>
 | 
						|
 | 
						|
    Args:
 | 
						|
        bom_path: Path, destination Path for the BOM file
 | 
						|
        root_path: Path, root directory Path
 | 
						|
        mkbom_tool: Path, mkbom tool Path
 | 
						|
    """
 | 
						|
    print(f"Creating BOM file from {root_path} to {bom_path}")
 | 
						|
    subprocess.check_call(
 | 
						|
        [
 | 
						|
            mkbom_tool,
 | 
						|
            "-u",
 | 
						|
            "0",
 | 
						|
            "-g",
 | 
						|
            "80",
 | 
						|
            str(root_path),
 | 
						|
            str(bom_path),
 | 
						|
        ]
 | 
						|
    )
 | 
						|
    print(f"Created BOM File size: {bom_path.stat().st_size // 1024}kb")
 | 
						|
 | 
						|
 | 
						|
def get_relative_glob_list(source: Path, glob: str) -> List[str]:
 | 
						|
    """
 | 
						|
    Given a source path, return a list of relative path based on glob
 | 
						|
 | 
						|
    Args:
 | 
						|
        source: Path, source directory Path
 | 
						|
        glob: str, unix style glob
 | 
						|
 | 
						|
    Returns:
 | 
						|
        list[str], paths found in source directory
 | 
						|
    """
 | 
						|
    return [f"./{c.relative_to(source)}" for c in source.glob(glob)]
 | 
						|
 | 
						|
 | 
						|
def xar_package_folder(source_path: Path, destination: Path, xar_tool: Path):
 | 
						|
    """
 | 
						|
    Create a pkg from <source_path> to <destination>
 | 
						|
    The command is issued with <source_path> as cwd
 | 
						|
 | 
						|
    Args:
 | 
						|
        source_path: Path, source absolute Path
 | 
						|
        destination: Path, destination absolute Path
 | 
						|
        xar_tool: Path, xar tool Path
 | 
						|
    """
 | 
						|
    if not source_path.is_absolute() or not destination.is_absolute():
 | 
						|
        raise Exception("Source and destination should be absolute.")
 | 
						|
 | 
						|
    print(f"Creating pkg from {source_path} to {destination}")
 | 
						|
    # Create a list of ./<file> - noting xar takes care of <file>/**
 | 
						|
    file_list = get_relative_glob_list(source_path, "*")
 | 
						|
 | 
						|
    subprocess.check_call(
 | 
						|
        [
 | 
						|
            xar_tool,
 | 
						|
            "--compression",
 | 
						|
            "none",
 | 
						|
            "-vcf",
 | 
						|
            destination,
 | 
						|
            *file_list,
 | 
						|
        ],
 | 
						|
        cwd=source_path,
 | 
						|
    )
 | 
						|
    print(f"Created PKG file to {destination}")
 | 
						|
    print(f"Created PKG size: {destination.stat().st_size // 1024}kb")
 | 
						|
 | 
						|
 | 
						|
def create_pkg(
 | 
						|
    source_app: Path,
 | 
						|
    output_pkg: Path,
 | 
						|
    mkbom_tool: Path,
 | 
						|
    xar_tool: Path,
 | 
						|
    cpio_tool: Path,
 | 
						|
):
 | 
						|
    """
 | 
						|
    Create a mac PKG installer from <source_app> to <output_pkg>
 | 
						|
 | 
						|
    Args:
 | 
						|
        source_app: Path, source .app file/directory Path
 | 
						|
        output_pkg: Path, destination .pkg file
 | 
						|
        mkbom_tool: Path, mkbom tool Path
 | 
						|
        xar_tool: Path, xar tool Path
 | 
						|
        cpio: Path, cpio tool Path
 | 
						|
    """
 | 
						|
 | 
						|
    app_name = source_app.name.rsplit(".", maxsplit=1)[0]
 | 
						|
 | 
						|
    with mozfile.TemporaryDirectory() as tmpdir:
 | 
						|
        root_path = Path(tmpdir) / "darwin/root"
 | 
						|
        flat_path = Path(tmpdir) / "darwin/flat"
 | 
						|
 | 
						|
        # Create required directories
 | 
						|
        # TODO: Investigate Resources folder contents for other lproj?
 | 
						|
        (flat_path / "Resources/en.lproj").mkdir(parents=True, exist_ok=True)
 | 
						|
        (flat_path / f"{app_name}.pkg").mkdir(parents=True, exist_ok=True)
 | 
						|
        root_path.mkdir(parents=True, exist_ok=True)
 | 
						|
 | 
						|
        # Copy files over
 | 
						|
        subprocess.check_call(
 | 
						|
            [
 | 
						|
                "cp",
 | 
						|
                "-R",
 | 
						|
                str(source_app),
 | 
						|
                str(root_path),
 | 
						|
            ]
 | 
						|
        )
 | 
						|
 | 
						|
        # Count all files (innards + itself)
 | 
						|
        file_count = len(list(source_app.glob("**/*"))) + 1
 | 
						|
        print(f"Calculated source files count: {file_count}")
 | 
						|
        # Get package contents size
 | 
						|
        package_size = sum(f.stat().st_size for f in source_app.glob("**/*")) // 1024
 | 
						|
        print(f"Calculated source package size: {package_size}kb")
 | 
						|
 | 
						|
        app_info = get_app_info_plist(source_app)
 | 
						|
        app_info["numberOfFiles"] = file_count
 | 
						|
        app_info["installKBytes"] = package_size
 | 
						|
        app_info["app_name"] = app_name
 | 
						|
        app_info["app_name_url_encoded"] = quote(app_name)
 | 
						|
 | 
						|
        # This seems arbitrary, there might be another way of doing it,
 | 
						|
        #   but Info.plist doesn't provide the simple version we need
 | 
						|
        major_version = app_info["CFBundleShortVersionString"].split(".")[0]
 | 
						|
        app_info["simple_version"] = f"{major_version}.0.0"
 | 
						|
 | 
						|
        pkg_info_tmpl = get_apple_template("PackageInfo.template")
 | 
						|
        pkg_info = pkg_info_tmpl.substitute(app_info)
 | 
						|
        save_text_file(pkg_info, flat_path / f"{app_name}.pkg/PackageInfo")
 | 
						|
 | 
						|
        distribution_tmp = get_apple_template("Distribution.template")
 | 
						|
        distribution = distribution_tmp.substitute(app_info)
 | 
						|
        save_text_file(distribution, flat_path / "Distribution")
 | 
						|
 | 
						|
        payload_path = flat_path / f"{app_name}.pkg/Payload"
 | 
						|
        create_payload(payload_path, root_path, cpio_tool)
 | 
						|
 | 
						|
        bom_path = flat_path / f"{app_name}.pkg/Bom"
 | 
						|
        create_bom(bom_path, root_path, mkbom_tool)
 | 
						|
 | 
						|
        xar_package_folder(flat_path, output_pkg, xar_tool)
 |