#!/usr/bin/env python3 """Build a macOS app image with a bundled runtime and isolated target tooling.""" import argparse, hashlib, json, os, platform, shutil, subprocess, tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent p=argparse.ArgumentParser(description=__doc__) p.add_argument('--java-home', type=Path, required=True) p.add_argument('--destination', type=Path, default=ROOT/'target/distribution') a=p.parse_args() if platform.system()!='Darwin': p.error('This packaging slice qualifies macOS only') java=a.java_home.resolve(); destination=a.destination.resolve() app=destination/'NeitzelFXDesigner.app' for output in (app,destination/'package-manifest.json',destination/'NeitzelFXDesigner-macos.zip'): if output.exists() or output.is_symlink(): p.error(f'Refusing to replace existing output: {output}') env=dict(os.environ, JAVA_HOME=str(java)) def run(cmd): subprocess.run(list(map(str,cmd)),cwd=ROOT,env=env,check=True) run([java/'bin/java','-version']) run([os.sys.executable,ROOT/'scripts/check-neitzellib-artifact.py']) run([ROOT/'mvnw','-B','-ntp','-pl','designer-standalone,designer-target-tools','-am','install']) run([ROOT/'mvnw','-B','-ntp','-pl','designer-standalone','org.apache.maven.plugins:maven-dependency-plugin:3.6.1:build-classpath','-DincludeScope=runtime','-Dmdep.outputFile=target/package-classpath.txt']) with tempfile.TemporaryDirectory(prefix='designer-package-') as tmp: stage=Path(tmp)/'input'; stage.mkdir() # Explicit manifest order: the verified Kit patch must precede upstream Kit. paths=[ROOT/'designer-standalone/target/designer-standalone-0.1.0-SNAPSHOT.jar'] deps=[Path(s) for s in (ROOT/'designer-standalone/target/package-classpath.txt').read_text().strip().split(os.pathsep)] paths += sorted(deps,key=lambda f:(0 if f.name.startswith('designer-kit-ui-') else 1,f.name)) for file in paths: if file.name.endswith('-tests.jar') or any(s in file.name for s in ('junit','intellij','target-tools','preview-api')): raise RuntimeError('Unexpected host dependency: '+str(file)) if (stage/file.name).exists(): raise RuntimeError('Duplicate artifact name: '+file.name) shutil.copy2(file,stage/file.name) manifest=Path(tmp)/'MANIFEST.MF' # Fold ASCII artifact names into legal manifest continuation lines. classpath='Class-Path: '+' '.join(f.name for f in paths) lines=[classpath[:70]] classpath=classpath[70:] while classpath: lines.append(' '+classpath[:69]); classpath=classpath[69:] manifest.write_text('Manifest-Version: 1.0\nMain-Class: de.neitzel.fxdesigner.standalone.Launcher\n'+'\n'.join(lines)+'\n\n') empty=Path(tmp)/'empty'; empty.mkdir() run([java/'bin/jar','cfm',stage/'launcher.jar',manifest,'-C',empty,'.']) tools=Path(tmp)/'tools'; tools.mkdir() for name in ('target-tools','contracts','preview-api'): file=ROOT/f'designer-{name}/target/designer-{name}-0.1.0-SNAPSHOT.jar' shutil.copy2(file,tools/file.name) shutil.copy2(ROOT/'THIRD-PARTY-NOTICES.md',stage) runtime=Path(tmp)/'runtime' run([java/'bin/jlink','--add-modules','java.se,jdk.unsupported,jdk.unsupported.desktop,jdk.xml.dom,jdk.crypto.ec,jdk.zipfs,jdk.localedata,jdk.management', '--strip-debug','--no-header-files','--no-man-pages','--output',runtime]) destination.mkdir(parents=True,exist_ok=True) run([java/'bin/jpackage','--type','app-image','--name','NeitzelFXDesigner','--app-version','1.0', '--vendor','Neitzel','--input',stage,'--main-jar','launcher.jar','--runtime-image',runtime, '--java-options','--enable-native-access=ALL-UNNAMED','--dest',destination]) # jpackage adds every input JAR recursively to the host classpath. Copy worker # artifacts only after it generates the launcher to keep the process boundary. shutil.copytree(tools,app/'Contents/app/tools') cfg=(app/'Contents/app/NeitzelFXDesigner.cfg').read_text() assert '/tools/' not in cfg, 'Target tools leaked into the host launcher' inventory={str(f.relative_to(app)):hashlib.sha256(f.read_bytes()).hexdigest() for f in sorted(app.rglob('*')) if f.is_file()} (destination/'package-manifest.json').write_text(json.dumps({'platform':platform.platform(),'architecture':platform.machine(), 'applicationVersion':'0.1.0-SNAPSHOT','bundleVersion':'1.0', 'source':subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip(), 'workingTreeDirty':bool(subprocess.check_output(['git','status','--porcelain'],cwd=ROOT,text=True).strip()), 'javaRelease':(java/'release').read_text(),'files':inventory},indent=2)+'\n') run(['/usr/bin/ditto','-c','-k','--sequesterRsrc','--keepParent',app,destination/'NeitzelFXDesigner-macos.zip']) print(app)