Skip to main content

scripts-build-platform-packages

#!/usr/bin/env python3 """

title: "Platform configuration" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Build npm platform packages for cross-platform binary distribution." keywords: ['build', 'git', 'packages', 'platform'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "build-platform-packages.py" language: python executable: true usage: "python3 scripts/build-platform-packages.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false

Build npm platform packages for cross-platform binary distribution.

This script creates platform-specific npm packages for the optionalDependencies pattern, embedding native binaries into properly structured npm packages.

Usage: python build-platform-packages.py --binary ./target/release/coditect
--scope @az1 --name coditect-core --version 1.0.0 --output ./dist

python build-platform-packages.py --all --binaries-dir ./target \
--scope @az1 --name coditect-core --version 1.0.0

Platforms: darwin-x64 macOS Intel darwin-arm64 macOS Apple Silicon linux-x64 Linux x64 (glibc) linux-arm64 Linux ARM64 linux-x64-musl Alpine Linux (musl) win32-x64 Windows x64 """

import argparse import json import os import shutil import subprocess import sys from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional

Platform configuration

PLATFORMS = { "darwin-x64": {"os": "darwin", "cpu": "x64", "binary_ext": ""}, "darwin-arm64": {"os": "darwin", "cpu": "arm64", "binary_ext": ""}, "linux-x64": {"os": "linux", "cpu": "x64", "binary_ext": ""}, "linux-arm64": {"os": "linux", "cpu": "arm64", "binary_ext": ""}, "linux-x64-musl": {"os": "linux", "cpu": "x64", "binary_ext": ""}, "win32-x64": {"os": "win32", "cpu": "x64", "binary_ext": ".exe"}, }

@dataclass class PackageConfig: """Configuration for building platform packages.""" scope: str name: str version: str description: str license: str repository: str author: str homepage: str

def create_platform_package_json( config: PackageConfig, platform: str, platform_info: Dict[str, str] ) -> Dict: """Create package.json for a platform-specific package.""" platform_name = f"{config.scope}/{config.name}-{platform}"

return {
"name": platform_name,
"version": config.version,
"description": f"{platform_info['os'].title()} {platform_info['cpu']} binary for {config.name}",
"license": config.license,
"author": config.author,
"repository": {
"type": "git",
"url": config.repository
},
"homepage": config.homepage,
"os": [platform_info["os"]],
"cpu": [platform_info["cpu"]],
"files": ["bin/"],
"preferUnplugged": True
}

def create_wrapper_package_json( config: PackageConfig, binary_name: str ) -> Dict: """Create package.json for the main wrapper package.""" optional_deps = { f"{config.scope}/{config.name}-{platform}": config.version for platform in PLATFORMS.keys() }

return {
"name": f"{config.scope}/{config.name}",
"version": config.version,
"description": config.description,
"license": config.license,
"author": config.author,
"repository": {
"type": "git",
"url": config.repository
},
"homepage": config.homepage,
"bin": {
binary_name: "bin/cli.js"
},
"main": "lib/index.js",
"files": [
"bin/",
"lib/",
"postinstall.js"
],
"scripts": {
"postinstall": "node postinstall.js"
},
"optionalDependencies": optional_deps,
"engines": {
"node": ">=18"
},
"os": ["darwin", "linux", "win32"],
"cpu": ["x64", "arm64"]
}

def build_platform_package( output_dir: Path, config: PackageConfig, platform: str, binary_path: Path, binary_name: str ) -> Path: """ Build a platform-specific npm package.

Args:
output_dir: Output directory for packages
config: Package configuration
platform: Platform key (e.g., 'darwin-arm64')
binary_path: Path to the native binary
binary_name: Name of the binary (without extension)

Returns:
Path to the created package directory
"""
platform_info = PLATFORMS[platform]
pkg_dir = output_dir / f"{config.name}-{platform}"

# Create package directory structure
bin_dir = pkg_dir / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)

# Copy binary with correct name
binary_ext = platform_info["binary_ext"]
dest_binary = bin_dir / f"{binary_name}{binary_ext}"
shutil.copy2(binary_path, dest_binary)

# Make executable on Unix
if platform_info["os"] != "win32":
dest_binary.chmod(0o755)

# Create package.json
pkg_json = create_platform_package_json(config, platform, platform_info)
with open(pkg_dir / "package.json", "w") as f:
json.dump(pkg_json, f, indent=2)

print(f" ✓ Built {platform} package: {pkg_dir}")
return pkg_dir

def build_wrapper_package( output_dir: Path, config: PackageConfig, binary_name: str, cdn_base: str ) -> Path: """ Build the main wrapper npm package.

Args:
output_dir: Output directory for packages
config: Package configuration
binary_name: Name of the binary/CLI command
cdn_base: Base URL for CDN (for postinstall fallback)

Returns:
Path to the created package directory
"""
pkg_dir = output_dir / config.name

# Create directory structure
(pkg_dir / "bin").mkdir(parents=True, exist_ok=True)
(pkg_dir / "lib").mkdir(parents=True, exist_ok=True)

# Create package.json
pkg_json = create_wrapper_package_json(config, binary_name)
with open(pkg_dir / "package.json", "w") as f:
json.dump(pkg_json, f, indent=2)

# Create bin/cli.js
cli_js = pkg_dir / "bin" / "cli.js"
cli_js.write_text(generate_cli_js(binary_name))
cli_js.chmod(0o755)

# Create lib/platform.js
(pkg_dir / "lib" / "platform.js").write_text(generate_platform_js())

# Create lib/binary.js
(pkg_dir / "lib" / "binary.js").write_text(
generate_binary_js(config.scope, config.name, binary_name)
)

# Create lib/index.js
(pkg_dir / "lib" / "index.js").write_text(
"module.exports = require('./binary');\n"
)

# Create postinstall.js
(pkg_dir / "postinstall.js").write_text(
generate_postinstall_js(cdn_base, binary_name)
)

print(f" ✓ Built wrapper package: {pkg_dir}")
return pkg_dir

def generate_cli_js(binary_name: str) -> str: """Generate bin/cli.js content.""" return f'''#!/usr/bin/env node const {{ spawn }} = require('child_process'); const {{ getBinaryPath }} = require('../lib/binary');

function main() {{ const binaryPath = getBinaryPath();

if (!binaryPath) {{ console.error('Error: Could not find {binary_name} binary for your platform.'); console.error(''); console.error('Try reinstalling the package:'); console.error(' npm uninstall -g '); console.error(' npm install -g '); process.exit(1); }}

const child = spawn(binaryPath, process.argv.slice(2), {{ stdio: 'inherit', windowsHide: true, }});

child.on('error', (err) => {{ console.error(Failed to start binary: ${{err.message}}); process.exit(1); }});

child.on('exit', (code, signal) => {{ if (signal) {{ process.kill(process.pid, signal); }} else {{ process.exit(code ?? 1); }} }}); }}

main(); '''

def generate_platform_js() -> str: """Generate lib/platform.js content.""" return '''const os = require('os'); const fs = require('fs');

function detectPlatform() { const platform = os.platform(); const arch = os.arch();

if (platform === 'linux' && arch === 'x64') { if (isMusl()) { return 'linux-x64-musl'; } }

return ${platform}-${arch}; }

function isMusl() { try { const osRelease = fs.readFileSync('/etc/os-release', 'utf8'); if (osRelease.includes('Alpine')) return true; } catch {}

try { const { execSync } = require('child_process'); const lddOutput = execSync('ldd --version 2>&1', { encoding: 'utf8' }); return lddOutput.toLowerCase().includes('musl'); } catch {}

try { return fs.existsSync('/lib/ld-musl-x86_64.so.1'); } catch {}

return false; }

module.exports = { detectPlatform, isMusl }; '''

def generate_binary_js(scope: str, name: str, binary_name: str) -> str: """Generate lib/binary.js content.""" return f'''const path = require('path'); const fs = require('fs'); const {{ detectPlatform }} = require('./platform');

const BINARY_NAME = '{binary_name}'; const SCOPE = '{scope}'; const NAME = '{name}';

function getBinaryPath() {{ const platform = detectPlatform(); const platformPkg = ${{SCOPE}}/${{NAME}}-${{platform}}; const binaryName = process.platform === 'win32' ? ${{BINARY_NAME}}.exe : BINARY_NAME;

const searchPaths = [ path.join(__dirname, '..', 'bin', binaryName), path.join(__dirname, '..', '..', ${{NAME}}-${{platform}}, 'bin', binaryName), path.join(__dirname, '..', 'node_modules', platformPkg, 'bin', binaryName), process.env.{binary_name.upper()}_BIN, ].filter(Boolean);

for (const searchPath of searchPaths) {{ if (fs.existsSync(searchPath)) {{ return searchPath; }} }}

return null; }}

module.exports = {{ getBinaryPath, BINARY_NAME }}; '''

def generate_postinstall_js(cdn_base: str, binary_name: str) -> str: """Generate postinstall.js content.""" return f'''#!/usr/bin/env node const https = require('https'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const {{ detectPlatform }} = require('./lib/platform'); const {{ getBinaryPath, BINARY_NAME }} = require('./lib/binary');

const CDN_BASE = '{cdn_base}';

async function main() {{ if (getBinaryPath()) {{ console.log('Binary found via optionalDependencies'); return; }}

console.log('Binary not found, downloading from CDN...');

try {{ const manifestUrl = ${{CDN_BASE}}/manifest.json; const manifest = await fetchJSON(manifestUrl); const platform = detectPlatform(); const release = manifest.platforms?.[platform];

if (!release) {{
throw new Error(`Unsupported platform: ${{platform}}`);
}}

const binDir = path.join(__dirname, 'bin');
fs.mkdirSync(binDir, {{ recursive: true }});

const binaryName = process.platform === 'win32'
? `${{BINARY_NAME}}.exe`
: BINARY_NAME;
const binaryPath = path.join(binDir, binaryName);

await downloadFile(release.url, binaryPath);

const actualHash = await hashFile(binaryPath);
if (actualHash !== release.sha256) {{
fs.unlinkSync(binaryPath);
throw new Error('Checksum verification failed');
}}

if (process.platform !== 'win32') {{
fs.chmodSync(binaryPath, 0o755);
}}

console.log('Binary installed successfully');

}} catch (err) {{ console.error(Warning: Failed to download binary: ${{err.message}}); }} }}

function fetchJSON(url) {{ return new Promise((resolve, reject) => {{ https.get(url, (res) => {{ let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => {{ try {{ resolve(JSON.parse(data)); }} catch (e) {{ reject(e); }} }}); }}).on('error', reject); }}); }}

function downloadFile(url, dest) {{ return new Promise((resolve, reject) => {{ const file = fs.createWriteStream(dest); https.get(url, (res) => {{ res.pipe(file); file.on('finish', () => {{ file.close(); resolve(); }}); }}).on('error', (err) => {{ fs.unlink(dest, () => {{}}); reject(err); }}); }}); }}

function hashFile(filePath) {{ return new Promise((resolve, reject) => {{ const hash = crypto.createHash('sha256'); const stream = fs.createReadStream(filePath); stream.on('data', data => hash.update(data)); stream.on('end', () => resolve(hash.digest('hex'))); stream.on('error', reject); }}); }}

main(); '''

def npm_pack(pkg_dir: Path) -> Path: """Run npm pack on a package directory.""" result = subprocess.run( ["npm", "pack"], cwd=pkg_dir, capture_output=True, text=True )

if result.returncode != 0:
raise RuntimeError(f"npm pack failed: {result.stderr}")

# Find the created tarball
tarball_name = result.stdout.strip()
return pkg_dir / tarball_name

def main(): parser = argparse.ArgumentParser( description="Build npm platform packages for cross-platform binary distribution", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=doc )

parser.add_argument(
"--scope", required=True,
help="npm organization scope (e.g., @az1)"
)
parser.add_argument(
"--name", required=True,
help="Package name (e.g., coditect-core)"
)
parser.add_argument(
"--version", required=True,
help="Package version (e.g., 1.0.0)"
)
parser.add_argument(
"--binary-name", required=True,
help="Binary/CLI command name (e.g., coditect)"
)
parser.add_argument(
"--output", "-o", type=Path, default=Path("./dist"),
help="Output directory (default: ./dist)"
)
parser.add_argument(
"--cdn-base", default="https://dist.example.com",
help="CDN base URL for postinstall fallback"
)

# Platform-specific options
platform_group = parser.add_mutually_exclusive_group(required=True)
platform_group.add_argument(
"--platform",
choices=list(PLATFORMS.keys()),
help="Build single platform package"
)
platform_group.add_argument(
"--all", action="store_true",
help="Build all platform packages"
)
platform_group.add_argument(
"--wrapper-only", action="store_true",
help="Build wrapper package only"
)

parser.add_argument(
"--binary", type=Path,
help="Path to binary (for single platform)"
)
parser.add_argument(
"--binaries-dir", type=Path,
help="Directory containing binaries (for --all)"
)

parser.add_argument(
"--description", default="Cross-platform CLI tool",
help="Package description"
)
parser.add_argument(
"--license", default="MIT",
help="Package license"
)
parser.add_argument(
"--repository", default="",
help="Repository URL"
)
parser.add_argument(
"--author", default="",
help="Package author"
)
parser.add_argument(
"--homepage", default="",
help="Package homepage"
)

parser.add_argument(
"--pack", action="store_true",
help="Run npm pack on generated packages"
)
parser.add_argument(
"--dry-run", action="store_true",
help="Print what would be done without creating files"
)

args = parser.parse_args()

# Validate arguments
if args.platform and not args.binary:
parser.error("--binary is required when using --platform")

if args.all and not args.binaries_dir:
parser.error("--binaries-dir is required when using --all")

# Create configuration
config = PackageConfig(
scope=args.scope,
name=args.name,
version=args.version,
description=args.description,
license=args.license,
repository=args.repository,
author=args.author,
homepage=args.homepage
)

# Create output directory
args.output.mkdir(parents=True, exist_ok=True)

print(f"Building packages for {config.scope}/{config.name}@{config.version}")
print(f"Output directory: {args.output}")
print()

packages_built = []

if args.wrapper_only:
# Build wrapper package only
pkg_dir = build_wrapper_package(
args.output, config, args.binary_name, args.cdn_base
)
packages_built.append(pkg_dir)

elif args.platform:
# Build single platform package
pkg_dir = build_platform_package(
args.output, config, args.platform,
args.binary, args.binary_name
)
packages_built.append(pkg_dir)

elif args.all:
# Build all platform packages
for platform in PLATFORMS.keys():
ext = PLATFORMS[platform]["binary_ext"]
binary_filename = f"{args.binary_name}-{platform}{ext}"
binary_path = args.binaries_dir / binary_filename

if not binary_path.exists():
print(f" ⚠ Skipping {platform}: binary not found at {binary_path}")
continue

pkg_dir = build_platform_package(
args.output, config, platform,
binary_path, args.binary_name
)
packages_built.append(pkg_dir)

# Also build wrapper package
pkg_dir = build_wrapper_package(
args.output, config, args.binary_name, args.cdn_base
)
packages_built.append(pkg_dir)

# Run npm pack if requested
if args.pack:
print()
print("Running npm pack...")
for pkg_dir in packages_built:
try:
tarball = npm_pack(pkg_dir)
print(f" ✓ Packed: {tarball}")
except RuntimeError as e:
print(f" ✗ Failed to pack {pkg_dir}: {e}")

print()
print(f"Built {len(packages_built)} packages successfully")

if name == "main": main()