Skip to content

Commit b3f4d16

Browse files
authored
Build ddev flat package for macOS (#15851)
1 parent 3632e04 commit b3f4d16

File tree

10 files changed

+338
-81
lines changed

10 files changed

+338
-81
lines changed

.github/workflows/build-ddev.yml

Lines changed: 35 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,6 @@ jobs:
378378
- name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }}
379379
run: pip install pyoxidizer==${{ env.PYOXIDIZER_VERSION }}
380380

381-
- name: Install create-dmg
382-
run: brew install create-dmg
383-
384381
# TODO: Use the next official release after 0.22.0 by removing these 2 blocks, uncommenting
385382
# the following one, and changing the artifact name to reflect the next version. See:
386383
# https://github.com/indygreg/apple-platform-rs/issues/82
@@ -416,10 +413,14 @@ jobs:
416413
env:
417414
APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE }}"
418415
APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY }}"
416+
APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE }}"
417+
APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY }}"
419418
APPLE_APP_STORE_CONNECT_API_DATA: "${{ secrets.APPLE_APP_STORE_CONNECT_API_DATA }}"
420419
run: |-
421-
echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate.pem
422-
echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key.pem
420+
echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate-application.pem
421+
echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key-application.pem
422+
echo "$APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE" > /tmp/certificate-installer.pem
423+
echo "$APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY" > /tmp/private-key-installer.pem
423424
echo "$APPLE_APP_STORE_CONNECT_API_DATA" > /tmp/app-store-connect.json
424425
425426
# We cannot use anchors because of https://github.com/actions/runner/issues/1182 and
@@ -444,8 +445,8 @@ jobs:
444445
script<<INNER
445446
for f in bin/*; do
446447
rcodesign sign -vv \
447-
--pem-source /tmp/certificate.pem \
448-
--pem-source /tmp/private-key.pem \
448+
--pem-source /tmp/certificate-application.pem \
449+
--pem-source /tmp/private-key-application.pem \
449450
--code-signature-flags runtime \
450451
"$f"
451452
done
@@ -535,65 +536,53 @@ jobs:
535536
fi
536537
done
537538
538-
- name: Build app bundle
539+
- name: Build universal binary
539540
run: >-
540-
pyoxidizer build macos_app_bundle
541+
pyoxidizer build macos_universal_binary
541542
--release
542543
--var version ${{ env.VERSION }}
543544
544-
- name: Stage app bundle
545-
id: stage
545+
- name: Prepare universal binary
546+
id: binary
546547
run: |-
547-
mkdir staged
548-
mkdir signed
549-
mv build/*/release/*/*.app staged
550-
app_bundle="$(ls staged)"
551-
app_name="${app_bundle:0:${#app_bundle}-4}"
548+
binary=$(echo build/*/release/*/${{ env.APP_NAME }})
549+
chmod +x "$binary"
550+
echo "path=$binary" >> "$GITHUB_OUTPUT"
552551
553-
echo "app-bundle=$app_bundle" >> "$GITHUB_OUTPUT"
554-
echo "app-name=$app_name-${{ env.VERSION }}.dmg" >> "$GITHUB_OUTPUT"
555-
echo "dmg-file=$app_name-${{ env.VERSION }}.dmg" >> "$GITHUB_OUTPUT"
556-
557-
- name: Sign app bundle
552+
- name: Build PKG
558553
run: >-
559-
rcodesign sign -vv
560-
--pem-source /tmp/certificate.pem
561-
--pem-source /tmp/private-key.pem
562-
"staged/${{ steps.stage.outputs.app-bundle }}"
563-
"signed/${{ steps.stage.outputs.app-bundle }}"
554+
python release/macos/build_pkg.py
555+
--binary ${{ steps.binary.outputs.path }}
556+
--version ${{ env.VERSION }}
557+
staged
564558
565-
- name: Create DMG
566-
run: >-
567-
create-dmg
568-
--volname "${{ steps.stage.outputs.app-name }}"
569-
--hide-extension "${{ steps.stage.outputs.app-bundle }}"
570-
--window-pos 200 120
571-
--window-size 800 400
572-
--icon-size 100
573-
--app-drop-link 600 185
574-
"${{ steps.stage.outputs.dmg-file }}"
575-
signed
576-
577-
- name: Sign DMG
559+
- name: Stage PKG
560+
id: pkg
561+
run: |-
562+
mkdir signed
563+
pkg_file="$(ls staged)"
564+
echo "path=$pkg_file" >> "$GITHUB_OUTPUT"
565+
566+
- name: Sign PKG
578567
run: >-
579568
rcodesign sign -vv
580-
--pem-source /tmp/certificate.pem
581-
--pem-source /tmp/private-key.pem
582-
"${{ steps.stage.outputs.dmg-file }}"
583-
"${{ steps.stage.outputs.dmg-file }}"
569+
--pem-source /tmp/certificate-installer.pem
570+
--pem-source /tmp/private-key-installer.pem
571+
"staged/${{ steps.pkg.outputs.path }}"
572+
"signed/${{ steps.pkg.outputs.path }}"
584573
585-
- name: Notarize DMG
574+
- name: Notarize PKG
586575
run: >-
587576
rcodesign notary-submit
588577
--api-key-path /tmp/app-store-connect.json
589578
--staple
590-
"${{ steps.stage.outputs.dmg-file }}"
579+
"signed/${{ steps.pkg.outputs.path }}"
591580
592581
- name: Upload installer
593582
uses: actions/upload-artifact@v3
594583
with:
595584
name: installers
596-
path: ddev/${{ steps.stage.outputs.dmg-file }}
585+
path: ddev/signed/${{ steps.pkg.outputs.path }}
597586

598587
publish:
599588
name: Publish release

ddev/pyoxidizer.bzl

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,7 @@ def make_exe_installer():
6767
return bundle
6868

6969

70-
def make_macos_app_bundle():
71-
# https://gregoryszorc.com/docs/pyoxidizer/main/tugger_starlark_type_macos_application_bundle_builder.html
72-
bundle = MacOsApplicationBundleBuilder(DISPLAY_NAME)
73-
bundle.set_info_plist_required_keys(
74-
display_name=DISPLAY_NAME,
75-
identifier="com.datadoghq." + APP_NAME,
76-
version=VERSION,
77-
signature=APP_NAME,
78-
executable=APP_NAME,
79-
)
80-
70+
def make_macos_universal_binary():
8171
# https://gregoryszorc.com/docs/pyoxidizer/main/tugger_starlark_type_apple_universal_binary.html
8272
universal = AppleUniversalBinary(APP_NAME)
8373

@@ -86,12 +76,10 @@ def make_macos_app_bundle():
8676

8777
m = FileManifest()
8878
m.add_file(universal.to_file_content())
89-
bundle.add_macos_manifest(m)
90-
91-
return bundle
79+
return m
9280

9381

9482
register_target("windows_installers", make_exe_installer, default=True)
95-
register_target("macos_app_bundle", make_macos_app_bundle)
83+
register_target("macos_universal_binary", make_macos_universal_binary)
9684

9785
resolve_targets()

ddev/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--mat
6464
[tool.hatch.build.hooks.vcs]
6565
version-file = "src/ddev/_version.py"
6666

67+
[tool.hatch.build.targets.sdist]
68+
include = ["src"]
69+
6770
[tool.hatch.build.targets.app]
6871
scripts = ["ddev"]
6972

ddev/release/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ddev release assets
2+
3+
-----
4+
5+
This directory stores files related to building binaries and installers for each platform.

ddev/release/macos/build_pkg.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# (C) Datadog, Inc. 2023-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
"""
5+
This script must be run from within the `ddev` directory.
6+
7+
At a high level, the goal is to have a directory that emulates the full path structure of the
8+
target machine which then gets packaged by tools that are only available on macOS.
9+
"""
10+
from __future__ import annotations
11+
12+
import argparse
13+
import shutil
14+
import subprocess
15+
import sys
16+
from pathlib import Path
17+
from tempfile import TemporaryDirectory
18+
19+
REPO_DIR = Path.cwd().parent
20+
ASSETS_DIR = Path(__file__).parent / 'pkg'
21+
IDENTIFIER = 'com.datadoghq.ddev'
22+
COMPONENT_PACKAGE_NAME = f'{IDENTIFIER}.pkg'
23+
README = """\
24+
<!DOCTYPE html>
25+
<html>
26+
<head></head>
27+
<body>
28+
<p>This will install ddev v{version} globally.</p>
29+
30+
<p>For more information on installing and upgrading ddev, see our <a href="https://datadoghq.dev/integrations-core/setup/#ddev">Installation Guide</a>.</p>
31+
</body>
32+
</html>
33+
""" # noqa: E501
34+
35+
36+
def run_command(command: list[str]) -> None:
37+
process = subprocess.run(command)
38+
if process.returncode:
39+
sys.exit(process.returncode)
40+
41+
42+
def main():
43+
parser = argparse.ArgumentParser()
44+
parser.add_argument('directory')
45+
parser.add_argument('--binary', required=True)
46+
parser.add_argument('--version', required=True)
47+
args = parser.parse_args()
48+
49+
directory = Path(args.directory).absolute()
50+
staged_binary = Path(args.binary).absolute()
51+
binary_name = staged_binary.stem
52+
version = args.version
53+
54+
with TemporaryDirectory() as d:
55+
temp_dir = Path(d)
56+
57+
# This is where we assemble files required for builds
58+
resources_dir = temp_dir / 'resources'
59+
shutil.copytree(str(ASSETS_DIR / 'resources'), str(resources_dir))
60+
61+
resources_dir.joinpath('README.html').write_text(README.format(version=version), encoding='utf-8')
62+
shutil.copy2(REPO_DIR / 'LICENSE', resources_dir)
63+
64+
# This is what gets shipped to users starting at / (the root directory)
65+
root_dir = temp_dir / 'root'
66+
root_dir.mkdir()
67+
68+
# This is where we globally install ddev. We choose to not offer per-user installs because we can't
69+
# find out where the location is and therefore cannot add to PATH usually. For more information, see:
70+
# https://github.com/aws/aws-cli/commit/f3c3eb8262786142a1712b6da5a1515ad9dc66c5
71+
relative_binary_dir = Path('usr', 'local', binary_name, 'bin')
72+
binary_dir = root_dir / relative_binary_dir
73+
binary_dir.mkdir(parents=True)
74+
shutil.copy2(staged_binary, binary_dir)
75+
76+
# This is how we add the installation directory to PATH and is also what Go does,
77+
# although there are some caveats: https://apple.stackexchange.com/q/126725
78+
path_file = root_dir / 'etc' / 'paths.d' / binary_name
79+
path_file.parent.mkdir(parents=True)
80+
path_file.write_text(f'/{relative_binary_dir}\n', encoding='utf-8')
81+
82+
# This is where we build the intermediate components
83+
components_dir = temp_dir / 'components'
84+
components_dir.mkdir()
85+
86+
run_command(
87+
[
88+
'pkgbuild',
89+
'--root',
90+
str(root_dir),
91+
'--identifier',
92+
IDENTIFIER,
93+
'--version',
94+
version,
95+
'--install-location',
96+
'/',
97+
str(components_dir / COMPONENT_PACKAGE_NAME),
98+
]
99+
)
100+
101+
# This is where we build the final artifact
102+
build_dir = temp_dir / 'build'
103+
build_dir.mkdir()
104+
product_archive = build_dir / f'{binary_name}-{version}.pkg'
105+
106+
run_command(
107+
[
108+
'productbuild',
109+
'--distribution',
110+
str(ASSETS_DIR / 'distribution.xml'),
111+
'--resources',
112+
str(resources_dir),
113+
'--package-path',
114+
str(components_dir),
115+
str(product_archive),
116+
]
117+
)
118+
119+
# Copy the final artifact to the target directory
120+
directory.mkdir(parents=True, exist_ok=True)
121+
shutil.copy2(product_archive, directory)
122+
123+
124+
if __name__ == '__main__':
125+
main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
(C) Datadog, Inc. 2023-present
4+
All rights reserved
5+
Licensed under a 3-clause BSD style license (see LICENSE)
6+
-->
7+
8+
<installer-gui-script minSpecVersion="1">
9+
<!--
10+
https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
11+
-->
12+
<title>Datadog Agent integration developer tool</title>
13+
<license file="LICENSE" mime-type="text/plain"/>
14+
<readme file="README.html" mime-type="text/html"/>
15+
<background mime-type="image/png" file="icon.png" alignment="left" scaling="proportional"/>
16+
<background-darkAqua mime-type="image/png" file="icon.png" alignment="left" scaling="proportional"/>
17+
<options hostArchitectures="arm64,x86_64" customize="never" require-scripts="false"/>
18+
<domains enable_localSystem="true"/>
19+
20+
<choices-outline>
21+
<line choice="com.datadoghq.ddev.choice"/>
22+
</choices-outline>
23+
<choice title="ddev (universal)" id="com.datadoghq.ddev.choice">
24+
<pkg-ref id="com.datadoghq.ddev.pkg"/>
25+
</choice>
26+
27+
<pkg-ref id="com.datadoghq.ddev.pkg">com.datadoghq.ddev.pkg</pkg-ref>
28+
</installer-gui-script>
84.7 KB
Loading

docs/developer/.hooks/ddev_version.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import os
2+
import subprocess
3+
from functools import cache
4+
from pathlib import Path
5+
6+
MARKER = '<docs-insert-ddev-version>'
7+
SEMVER_PARTS = 3
8+
9+
# Ignore the current documentation environment so that the version
10+
# command can execute as usual in the default build environment
11+
os.environ.pop('HATCH_ENV_ACTIVE', None)
12+
13+
14+
@cache
15+
def get_latest_version():
16+
"""This returns the latest version of ddev."""
17+
ddev_root = Path.cwd() / 'ddev'
18+
output = subprocess.check_output(['hatch', 'version'], cwd=str(ddev_root)).decode('utf-8').strip()
19+
20+
version = output.replace('dev', '')
21+
parts = list(map(int, version.split('.')))
22+
major, minor, patch = parts[:SEMVER_PARTS]
23+
if len(parts) > SEMVER_PARTS:
24+
patch -= 1
25+
26+
return f'{major}.{minor}.{patch}'
27+
28+
29+
def on_page_read_source(page, config):
30+
"""This inserts the latest version of ddev."""
31+
with open(page.file.abs_src_path, encoding='utf-8') as f:
32+
return f.read().replace(MARKER, get_latest_version())

0 commit comments

Comments
 (0)