From fcb9e2ef516cff0b6d5168bc6465e4615d2aa96e Mon Sep 17 00:00:00 2001 From: _ov4 Date: Wed, 21 Oct 2026 11:22:38 +0000 Subject: ci(bundle): publish portable Windows and macOS desktop packages Build and check Bundle alongside each native compiler. Publish a single Windows exe with an embedded compiler extracted into the startup directory, and a macOS tar.xz containing the app and compiler. Check Linux without publishing a desktop package and require all six nightly attachments. Include licenses in embedded assets and app resources. Verify local builds, package smoke checks and release replacement with a simulated GitHub API. Co-Authored-By: Claude Opus 5.5 --- bundle/scripts/package.js | 40 +++++++++++++++++++++++++++++++++ bundle/scripts/prepare-compiler.js | 11 +++++++--- bundle/scripts/smoke-package.py | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 bundle/scripts/package.js create mode 100644 bundle/scripts/smoke-package.py (limited to 'bundle/scripts') diff --git a/bundle/scripts/package.js b/bundle/scripts/package.js new file mode 100644 index 0000000..56aae85 --- /dev/null +++ b/bundle/scripts/package.js @@ -0,0 +1,40 @@ +import { execFileSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); + +try { + const platform = { + win32: { architecture: 'x64', name: 'Windows-x86_64', extension: '.exe' }, + darwin: { architecture: 'arm64', name: 'Darwin-arm64', extension: '.tar.xz' } + }[process.platform]; + if (!platform || process.arch !== platform.architecture) { + throw new Error('Release packages are built on Windows x64 and macOS arm64 only. Use tauri build --no-bundle for Linux.'); + } + const revision = execFileSync('git', ['log', '-1', '--format=%ct:%h'], { cwd: root, encoding: 'utf8' }).trim(); + const match = /^(\d+):([0-9a-f]+)$/.exec(revision); + if (!match) throw new Error('Cannot read HEAD for the package name.'); + const timestamp = new Date(Number(match[1]) * 1000).toISOString(); + const date = timestamp.slice(0, 10).replace(/-/g, ''); + const time = timestamp.slice(11, 19).replace(/:/g, ''); + execFileSync(process.execPath, [path.join(root, 'node_modules/@tauri-apps/cli/tauri.js'), 'build', + ...(process.platform === 'win32' ? ['--no-bundle'] : ['--bundles', 'app'])], { cwd: root, stdio: 'inherit' }); + const artifacts = path.join(root, 'artifacts'); + rmSync(artifacts, { recursive: true, force: true }); + mkdirSync(artifacts, { recursive: true }); + const name = `cpsc-bundle-${date}-${time}-${match[2]}-${platform.name}${platform.extension}`; + if (process.platform === 'win32') { + copyFileSync(path.join(root, 'src-tauri/target/release/cpsc-bundle.exe'), path.join(artifacts, name)); + } else { + const built = path.join(root, 'src-tauri/target/release/bundle/macos'); + const apps = readdirSync(built).filter(name => name.endsWith('.app')); + if (apps.length !== 1) throw new Error(`Expected one .app, found ${apps.length}.`); + execFileSync('tar', ['-cJf', path.join(artifacts, name), '-C', built, apps[0]], { stdio: 'inherit' }); + } + console.log(`Packaged: ${name}`); +} catch (error) { + console.error(`Cannot package Bundle: ${error.message}`); + process.exitCode = 1; +} diff --git a/bundle/scripts/prepare-compiler.js b/bundle/scripts/prepare-compiler.js index de5346a..8568ae9 100644 --- a/bundle/scripts/prepare-compiler.js +++ b/bundle/scripts/prepare-compiler.js @@ -1,5 +1,6 @@ import { execFileSync } from 'node:child_process'; -import { copyFileSync, mkdirSync } from 'node:fs'; +import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { gzipSync } from 'node:zlib'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -7,7 +8,11 @@ const root = fileURLToPath(new URL('..', import.meta.url)); const target = execFileSync('rustc', ['--print', 'host-tuple'], { encoding: 'utf8' }).trim(); const extension = process.platform === 'win32' ? '.exe' : ''; const source = path.resolve(process.argv[2] || path.join(root, '..', 'build', `cpsc${extension}`)); -const destination = path.join(root, 'src-tauri', 'binaries', `cpsc-${target}${extension}`); +const destination = path.join(root, 'src-tauri', 'binaries', process.platform === 'win32' ? 'cpsc.exe.gz' : `cpsc-${target}${extension}`); mkdirSync(path.dirname(destination), { recursive: true }); -copyFileSync(source, destination); +if (process.platform === 'win32') { + writeFileSync(destination, gzipSync(readFileSync(source))); +} else { + copyFileSync(source, destination); +} console.log(`Bundled compiler: ${source}`); diff --git a/bundle/scripts/smoke-package.py b/bundle/scripts/smoke-package.py new file mode 100644 index 0000000..98ee52a --- /dev/null +++ b/bundle/scripts/smoke-package.py @@ -0,0 +1,45 @@ +"""Check a portable desktop package without relying on an installed toolchain.""" + +from pathlib import Path +import os +import subprocess +import sys +import tempfile +import time + +root = Path(sys.argv[1]).resolve() +environment = os.environ.copy() +environment["PATH"] = os.pathsep.join((os.path.join(environment["SystemRoot"], "System32"), environment["SystemRoot"])) if sys.platform == "win32" else "/nonexistent" +with tempfile.TemporaryDirectory(prefix="cpsc bundle smoke ") as directory: + work = Path(directory) + if sys.platform == "win32": + assert root.is_file() and root.suffix == ".exe", "Expected a single portable .exe" + # Extraction happens before WebView startup and publishes a complete file by rename. + desktop = subprocess.Popen([str(root)], cwd=work, env=environment) + compiler = work / "cpsc.exe" + try: + deadline = time.monotonic() + 30 + while not compiler.is_file(): + assert desktop.poll() is None, "Desktop exited before extracting its compiler" + assert time.monotonic() < deadline, "Compiler extraction timed out" + time.sleep(0.1) + finally: + if desktop.poll() is None: + desktop.terminate() + desktop.wait(timeout=10) + else: + compilers = list(root.rglob("cpsc")) + assert len(compilers) == 1, f"Expected one packaged compiler, found {compilers}" + compiler = compilers[0] + assert compiler.with_name("cpsc-bundle").is_file(), "Missing desktop executable" + resources = compiler.parent.parent / "Resources" if sys.platform == "darwin" else compiler.parent + for filename in ("LICENSE", "ThirdPartyNotices.md", "licenses/vscode.txt", "licenses/plugin.md"): + assert (resources / filename).is_file(), f"Missing {filename}" + (work / "data.txt").write_text("relative file\n", encoding="utf-8") + source = work / "source file.cps" + source.write_text('DECLARE N : INTEGER\nINPUT N\nOUTPUT N\nDECLARE S : STRING\nOPENFILE "data.txt" FOR READ\nREADFILE "data.txt", S\nCLOSEFILE "data.txt"\nOUTPUT S\n', encoding="utf-8") + result = subprocess.run([str(compiler), str(source), "-r"], input=b"17\n", capture_output=True, cwd=work, env=environment, timeout=30) + assert result.returncode == 0, result.stderr + assert result.stdout.replace(b"\r\n", b"\n") == b"17\nrelative file\n", result.stdout + assert result.stderr == b"", result.stderr +print("Desktop package smoke passed: portable application, compiler, stdin and relative files.") -- cgit v1.3.1-4-g156e