Add publishers for distributing CLI binaries to package managers: - npm: binary wrapper pattern with postinstall download - Homebrew: formula generation + tap auto-commit - Scoop: JSON manifest + bucket auto-commit - AUR: PKGBUILD + .SRCINFO + AUR push - Chocolatey: NuSpec + install script + optional push Each publisher supports: - Dry-run mode for previewing changes - Auto-commit to own repos (tap/bucket/AUR) - Generate files for PRs to official repos via `official` config Also includes Docker and LinuxKit build helpers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
4.8 KiB
JavaScript
176 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Binary installer for {{.Package}}
|
|
* Downloads the correct binary for the current platform from GitHub releases.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
const { spawnSync } = require('child_process');
|
|
const crypto = require('crypto');
|
|
|
|
const PACKAGE_VERSION = '{{.Version}}';
|
|
const GITHUB_REPO = '{{.Repository}}';
|
|
const BINARY_NAME = '{{.BinaryName}}';
|
|
|
|
// Platform/arch mapping
|
|
const PLATFORM_MAP = {
|
|
darwin: 'darwin',
|
|
linux: 'linux',
|
|
win32: 'windows',
|
|
};
|
|
|
|
const ARCH_MAP = {
|
|
x64: 'amd64',
|
|
arm64: 'arm64',
|
|
};
|
|
|
|
function getPlatformInfo() {
|
|
const platform = PLATFORM_MAP[process.platform];
|
|
const arch = ARCH_MAP[process.arch];
|
|
|
|
if (!platform || !arch) {
|
|
console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
return { platform, arch };
|
|
}
|
|
|
|
function getDownloadUrl(platform, arch) {
|
|
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
|
|
const name = `${BINARY_NAME}-${platform}-${arch}${ext}`;
|
|
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/${name}`;
|
|
}
|
|
|
|
function getChecksumsUrl() {
|
|
return `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}/checksums.txt`;
|
|
}
|
|
|
|
function download(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = (url) => {
|
|
https.get(url, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
// Follow redirect
|
|
request(res.headers.location);
|
|
return;
|
|
}
|
|
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`Failed to download ${url}: HTTP ${res.statusCode}`));
|
|
return;
|
|
}
|
|
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
res.on('error', reject);
|
|
}).on('error', reject);
|
|
};
|
|
request(url);
|
|
});
|
|
}
|
|
|
|
async function fetchChecksums() {
|
|
try {
|
|
const data = await download(getChecksumsUrl());
|
|
const checksums = {};
|
|
data.toString().split('\n').forEach((line) => {
|
|
const parts = line.trim().split(/\s+/);
|
|
if (parts.length === 2) {
|
|
checksums[parts[1]] = parts[0];
|
|
}
|
|
});
|
|
return checksums;
|
|
} catch (err) {
|
|
console.warn('Warning: Could not fetch checksums, skipping verification');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function verifyChecksum(data, expectedHash) {
|
|
const actualHash = crypto.createHash('sha256').update(data).digest('hex');
|
|
return actualHash === expectedHash;
|
|
}
|
|
|
|
function extract(data, destDir, platform) {
|
|
const tempFile = path.join(destDir, platform === 'windows' ? 'temp.zip' : 'temp.tar.gz');
|
|
fs.writeFileSync(tempFile, data);
|
|
|
|
try {
|
|
if (platform === 'windows') {
|
|
// Use PowerShell to extract zip
|
|
const result = spawnSync('powershell', [
|
|
'-command',
|
|
`Expand-Archive -Path '${tempFile}' -DestinationPath '${destDir}' -Force`
|
|
], { stdio: 'ignore' });
|
|
if (result.status !== 0) {
|
|
throw new Error('Failed to extract zip');
|
|
}
|
|
} else {
|
|
const result = spawnSync('tar', ['-xzf', tempFile, '-C', destDir], { stdio: 'ignore' });
|
|
if (result.status !== 0) {
|
|
throw new Error('Failed to extract tar.gz');
|
|
}
|
|
}
|
|
} finally {
|
|
fs.unlinkSync(tempFile);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const { platform, arch } = getPlatformInfo();
|
|
const binDir = path.join(__dirname, 'bin');
|
|
const binaryPath = path.join(binDir, platform === 'windows' ? `${BINARY_NAME}.exe` : BINARY_NAME);
|
|
|
|
// Skip if binary already exists
|
|
if (fs.existsSync(binaryPath)) {
|
|
console.log(`${BINARY_NAME} binary already installed`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Installing ${BINARY_NAME} v${PACKAGE_VERSION} for ${platform}/${arch}...`);
|
|
|
|
// Ensure bin directory exists
|
|
if (!fs.existsSync(binDir)) {
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
}
|
|
|
|
// Fetch checksums
|
|
const checksums = await fetchChecksums();
|
|
|
|
// Download binary
|
|
const url = getDownloadUrl(platform, arch);
|
|
console.log(`Downloading from ${url}`);
|
|
|
|
const data = await download(url);
|
|
|
|
// Verify checksum if available
|
|
if (checksums) {
|
|
const ext = platform === 'windows' ? '.zip' : '.tar.gz';
|
|
const filename = `${BINARY_NAME}-${platform}-${arch}${ext}`;
|
|
const expectedHash = checksums[filename];
|
|
if (expectedHash && !verifyChecksum(data, expectedHash)) {
|
|
console.error('Checksum verification failed!');
|
|
process.exit(1);
|
|
}
|
|
console.log('Checksum verified');
|
|
}
|
|
|
|
// Extract
|
|
extract(data, binDir, platform);
|
|
|
|
// Make executable on Unix
|
|
if (platform !== 'windows') {
|
|
fs.chmodSync(binaryPath, 0o755);
|
|
}
|
|
|
|
console.log(`${BINARY_NAME} installed successfully`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`Installation failed: ${err.message}`);
|
|
process.exit(1);
|
|
});
|