pypa-hatch/release/macos/build_pkg.py

120 lines
3.9 KiB
Python

"""
This script must be run from the root of the repository.
At a high level, the goal is to have a directory that emulates the full path structure of the
target machine which then gets packaged by tools that are only available on macOS.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
REPO_DIR = Path.cwd()
ASSETS_DIR = Path(__file__).parent / 'pkg'
IDENTIFIER = 'org.python.hatch'
COMPONENT_PACKAGE_NAME = f'{IDENTIFIER}.pkg'
README = """\
<!DOCTYPE html>
<html>
<head></head>
<body>
<p>This will install Hatch v{version} globally.</p>
<p>For more information on installing and upgrading Hatch, see our <a href="https://hatch.pypa.io/latest/install/">Installation Guide</a>.</p>
</body>
</html>
"""
def run_command(command: list[str]) -> None:
process = subprocess.run(command) # noqa: PLW1510
if process.returncode:
sys.exit(process.returncode)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('directory')
parser.add_argument('--binary', required=True)
parser.add_argument('--version', required=True)
args = parser.parse_args()
directory = Path(args.directory).absolute()
staged_binary = Path(args.binary).absolute()
binary_name = staged_binary.stem
version = args.version
with TemporaryDirectory() as d:
temp_dir = Path(d)
# This is where we assemble files required for builds
resources_dir = temp_dir / 'resources'
shutil.copytree(str(ASSETS_DIR / 'resources'), str(resources_dir))
resources_dir.joinpath('README.html').write_text(README.format(version=version), encoding='utf-8')
shutil.copy2(REPO_DIR / 'LICENSE.txt', resources_dir)
# This is what gets shipped to users starting at / (the root directory)
root_dir = temp_dir / 'root'
root_dir.mkdir()
# This is where we globally install Hatch. We choose to not offer per-user installs because we can't
# find out where the location is and therefore cannot add to PATH usually. For more information, see:
# https://github.com/aws/aws-cli/commit/f3c3eb8262786142a1712b6da5a1515ad9dc66c5
relative_binary_dir = Path('usr', 'local', binary_name, 'bin')
binary_dir = root_dir / relative_binary_dir
binary_dir.mkdir(parents=True)
shutil.copy2(staged_binary, binary_dir)
# This is how we add the installation directory to PATH and is also what Go does,
# although there are some caveats: https://apple.stackexchange.com/q/126725
path_file = root_dir / 'etc' / 'paths.d' / binary_name
path_file.parent.mkdir(parents=True)
path_file.write_text(f'/{relative_binary_dir}\n', encoding='utf-8')
# This is where we build the intermediate components
components_dir = temp_dir / 'components'
components_dir.mkdir()
run_command([
'pkgbuild',
'--root',
str(root_dir),
'--identifier',
IDENTIFIER,
'--version',
version,
'--install-location',
'/',
str(components_dir / COMPONENT_PACKAGE_NAME),
])
# This is where we build the final artifact
build_dir = temp_dir / 'build'
build_dir.mkdir()
product_archive = build_dir / f'{binary_name}-universal.pkg'
run_command([
'productbuild',
'--distribution',
str(ASSETS_DIR / 'distribution.xml'),
'--resources',
str(resources_dir),
'--package-path',
str(components_dir),
str(product_archive),
])
# Copy the final artifact to the target directory
directory.mkdir(parents=True, exist_ok=True)
shutil.copy2(product_archive, directory)
if __name__ == '__main__':
main()