704 lines
21 KiB
JavaScript
704 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
const fs = require("fs");
|
|
const https = require("https");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawnSync } = require("child_process");
|
|
const { path7za } = require("7zip-bin");
|
|
|
|
const VERSION = process.env.IMAGEMAGICK_VERSION || "7.1.2-18";
|
|
const PLATFORM = process.platform;
|
|
const ARCH = process.arch;
|
|
const TARGET_DIR = path.join(__dirname, "..", "resources", "imagemagick", `${PLATFORM}-${ARCH}`);
|
|
const CACHE_DIR = path.join(__dirname, "..", ".cache", "imagemagick", VERSION);
|
|
const MANIFEST_NAME = "presenton-runtime.json";
|
|
|
|
function log(message) {
|
|
console.log(`[imagemagick] ${message}`);
|
|
}
|
|
|
|
function fail(message) {
|
|
console.error(`[imagemagick] ${message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
...options,
|
|
});
|
|
if (result.status !== 0) {
|
|
fail(`${command} ${args.join(" ")} failed with code ${result.status}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function capture(command, args, options = {}) {
|
|
return spawnSync(command, args, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
encoding: "utf8",
|
|
windowsHide: true,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function runtimeEnv(binaryPath) {
|
|
const homeDir = fs.statSync(binaryPath).isFile()
|
|
? path.dirname(binaryPath)
|
|
: binaryPath;
|
|
const tempDir = process.env.TEMP || process.env.TMPDIR || os.tmpdir() || homeDir;
|
|
return {
|
|
...process.env,
|
|
MAGICK_HOME: path.basename(homeDir).toLowerCase() === "bin"
|
|
? path.dirname(homeDir)
|
|
: homeDir,
|
|
MAGICK_CONFIGURE_PATH: path.basename(homeDir).toLowerCase() === "bin"
|
|
? path.dirname(homeDir)
|
|
: homeDir,
|
|
MAGICK_TEMPORARY_PATH: tempDir,
|
|
MAGICK_OCL_DEVICE: "OFF",
|
|
APPIMAGE_EXTRACT_AND_RUN: "1",
|
|
};
|
|
}
|
|
|
|
function versionOutput(binaryPath) {
|
|
const result = spawnSync(binaryPath, ["-version"], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
encoding: "utf8",
|
|
timeout: 120000,
|
|
env: runtimeEnv(binaryPath),
|
|
windowsHide: true,
|
|
});
|
|
if (result.status !== 0 || result.signal) {
|
|
const reason = result.error?.message
|
|
|| (result.stderr || "").trim()
|
|
|| (result.signal ? `terminated by signal ${result.signal}` : `exit ${result.status}`);
|
|
return { ok: false, reason };
|
|
}
|
|
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
return { ok: output.toLowerCase().includes("imagemagick"), output };
|
|
}
|
|
|
|
function readManifest(targetDir) {
|
|
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
if (!fs.existsSync(manifestPath)) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeManifest(targetDir, manifest) {
|
|
fs.writeFileSync(
|
|
path.join(targetDir, MANIFEST_NAME),
|
|
JSON.stringify(
|
|
{
|
|
version: VERSION,
|
|
platform: PLATFORM,
|
|
arch: ARCH,
|
|
createdAt: new Date().toISOString(),
|
|
...manifest,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
function validateRuntime(targetDir) {
|
|
const manifest = readManifest(targetDir);
|
|
const binary = manifest?.binary || (PLATFORM === "win32" ? "magick.exe" : "bin/magick");
|
|
const binaryPath = path.join(targetDir, binary);
|
|
if (!fs.existsSync(binaryPath)) {
|
|
return null;
|
|
}
|
|
if (PLATFORM !== "win32") {
|
|
try {
|
|
fs.chmodSync(binaryPath, 0o755);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
const result = versionOutput(binaryPath);
|
|
if (!result.ok) {
|
|
log(`Runtime validation failed for ${binaryPath}: ${result.reason}`);
|
|
return null;
|
|
}
|
|
return { binaryPath, output: result.output };
|
|
}
|
|
|
|
function archiveName() {
|
|
if (PLATFORM !== "win32") {
|
|
return null;
|
|
}
|
|
const archName = ARCH === "x64" ? "x64" : ARCH === "arm64" ? "arm64" : ARCH === "ia32" ? "x86" : null;
|
|
if (!archName) {
|
|
fail(`No bundled ImageMagick Windows asset configured for ${PLATFORM}-${ARCH}`);
|
|
}
|
|
return `ImageMagick-${VERSION}-portable-Q16-${archName}.7z`;
|
|
}
|
|
|
|
function linuxAppImageArch() {
|
|
if (PLATFORM !== "linux") {
|
|
return null;
|
|
}
|
|
if (ARCH !== "x64") {
|
|
fail(`No bundled ImageMagick Linux AppImage asset configured for ${PLATFORM}-${ARCH}`);
|
|
}
|
|
return "x86_64";
|
|
}
|
|
|
|
function linuxDefaultAppImageName() {
|
|
return `ImageMagick-${VERSION}-gcc-${linuxAppImageArch()}.AppImage`;
|
|
}
|
|
|
|
function parseAssetNameFromUrl(urlValue) {
|
|
try {
|
|
const pathname = new URL(urlValue).pathname;
|
|
const name = pathname.split("/").filter(Boolean).pop();
|
|
return name || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function downloadUrl(assetName) {
|
|
if (process.env.IMAGEMAGICK_DOWNLOAD_URL) {
|
|
return process.env.IMAGEMAGICK_DOWNLOAD_URL;
|
|
}
|
|
return `https://github.com/ImageMagick/ImageMagick/releases/download/${VERSION}/${assetName}`;
|
|
}
|
|
|
|
function downloadFile(url, destination) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = https.get(
|
|
url,
|
|
{ headers: { "User-Agent": "Presenton ImageMagick runtime fetcher" } },
|
|
(response) => {
|
|
if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
|
const location = response.headers.location;
|
|
if (!location) {
|
|
reject(new Error(`Redirect from ${url} did not include Location`));
|
|
return;
|
|
}
|
|
response.resume();
|
|
downloadFile(new URL(location, url).toString(), destination).then(resolve, reject);
|
|
return;
|
|
}
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
|
|
response.resume();
|
|
return;
|
|
}
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
const file = fs.createWriteStream(destination);
|
|
response.pipe(file);
|
|
file.on("finish", () => file.close(resolve));
|
|
file.on("error", reject);
|
|
},
|
|
);
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function fetchJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = https.get(
|
|
url,
|
|
{
|
|
headers: {
|
|
"User-Agent": "Presenton ImageMagick runtime fetcher",
|
|
Accept: "application/vnd.github+json",
|
|
},
|
|
},
|
|
(response) => {
|
|
if ([301, 302, 303, 307, 308].includes(response.statusCode || 0)) {
|
|
const location = response.headers.location;
|
|
if (!location) {
|
|
reject(new Error(`Redirect from ${url} did not include Location`));
|
|
return;
|
|
}
|
|
response.resume();
|
|
fetchJson(new URL(location, url).toString()).then(resolve, reject);
|
|
return;
|
|
}
|
|
if (response.statusCode !== 200) {
|
|
reject(new Error(`Request failed with HTTP ${response.statusCode}: ${url}`));
|
|
response.resume();
|
|
return;
|
|
}
|
|
|
|
const chunks = [];
|
|
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
response.on("end", () => {
|
|
try {
|
|
const body = Buffer.concat(chunks).toString("utf8");
|
|
resolve(JSON.parse(body));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
response.on("error", reject);
|
|
},
|
|
);
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function uniqueNonEmpty(values) {
|
|
return values.filter(Boolean).filter((value, index, all) => all.indexOf(value) === index);
|
|
}
|
|
|
|
async function linuxAppImageCandidates() {
|
|
const configuredAsset = process.env.IMAGEMAGICK_LINUX_ASSET_NAME?.trim();
|
|
if (configuredAsset) {
|
|
return [configuredAsset];
|
|
}
|
|
|
|
const arch = linuxAppImageArch();
|
|
const downloadOverride = process.env.IMAGEMAGICK_DOWNLOAD_URL?.trim();
|
|
if (downloadOverride) {
|
|
return [parseAssetNameFromUrl(downloadOverride) || linuxDefaultAppImageName()];
|
|
}
|
|
|
|
const fallbackName = linuxDefaultAppImageName();
|
|
const candidates = [fallbackName];
|
|
try {
|
|
const release = await fetchJson(`https://api.github.com/repos/ImageMagick/ImageMagick/releases/tags/${VERSION}`);
|
|
const assets = Array.isArray(release?.assets) ? release.assets : [];
|
|
const appImages = assets
|
|
.map((asset) => asset?.name)
|
|
.filter((name) => typeof name === "string" && name.endsWith(`-${arch}.AppImage`));
|
|
|
|
const scored = appImages
|
|
.map((name) => ({
|
|
name,
|
|
score: name === fallbackName
|
|
? 100
|
|
: name.includes(`-gcc-${arch}.AppImage`)
|
|
? 90
|
|
: name.includes(`-clang-${arch}.AppImage`)
|
|
? 80
|
|
: 70,
|
|
}))
|
|
.sort((a, b) => b.score - a.score)
|
|
.map((entry) => entry.name);
|
|
|
|
candidates.push(...scored);
|
|
} catch (error) {
|
|
log(`Could not resolve Linux AppImage assets from release metadata: ${error?.message || error}`);
|
|
}
|
|
|
|
return uniqueNonEmpty(candidates);
|
|
}
|
|
|
|
function findMagickDir(root) {
|
|
const stack = [root];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
if (entries.some((entry) => entry.isFile() && entry.name.toLowerCase() === "magick.exe")) {
|
|
return current;
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
stack.push(path.join(current, entry.name));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function prepareWindows() {
|
|
if (!path7za || !fs.existsSync(path7za)) {
|
|
fail("7zip-bin is unavailable; run npm install before preparing ImageMagick.");
|
|
}
|
|
|
|
const assetName = archiveName();
|
|
const archivePath = path.join(CACHE_DIR, assetName);
|
|
const extractDir = path.join(CACHE_DIR, "extract");
|
|
const tempTarget = `${TARGET_DIR}.tmp`;
|
|
|
|
if (!fs.existsSync(archivePath)) {
|
|
const url = downloadUrl(assetName);
|
|
log(`Downloading ${url}`);
|
|
await downloadFile(url, archivePath);
|
|
} else {
|
|
log(`Using cached archive: ${archivePath}`);
|
|
}
|
|
|
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
log(`Extracting ${archivePath}`);
|
|
run(path7za, ["x", archivePath, `-o${extractDir}`, "-y"]);
|
|
|
|
const magickDir = findMagickDir(extractDir);
|
|
if (!magickDir) {
|
|
fail("Extracted archive did not contain magick.exe");
|
|
}
|
|
|
|
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
fs.mkdirSync(path.dirname(tempTarget), { recursive: true });
|
|
fs.cpSync(magickDir, tempTarget, { recursive: true });
|
|
writeManifest(tempTarget, {
|
|
kind: "windows-portable",
|
|
binary: "magick.exe",
|
|
source: downloadUrl(assetName),
|
|
});
|
|
|
|
if (!validateRuntime(tempTarget)) {
|
|
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
|
}
|
|
|
|
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
|
fs.renameSync(tempTarget, TARGET_DIR);
|
|
log(`Prepared ${path.join(TARGET_DIR, "magick.exe")}`);
|
|
}
|
|
|
|
async function prepareLinux() {
|
|
const candidates = await linuxAppImageCandidates();
|
|
const tempTarget = `${TARGET_DIR}.tmp`;
|
|
let assetName = null;
|
|
let appImagePath = null;
|
|
let lastDownloadError = null;
|
|
|
|
for (const candidate of candidates) {
|
|
const candidatePath = path.join(CACHE_DIR, candidate);
|
|
if (fs.existsSync(candidatePath)) {
|
|
log(`Using cached AppImage: ${candidatePath}`);
|
|
assetName = candidate;
|
|
appImagePath = candidatePath;
|
|
break;
|
|
}
|
|
|
|
const url = downloadUrl(candidate);
|
|
log(`Downloading ${url}`);
|
|
try {
|
|
await downloadFile(url, candidatePath);
|
|
assetName = candidate;
|
|
appImagePath = candidatePath;
|
|
break;
|
|
} catch (error) {
|
|
lastDownloadError = error;
|
|
const message = String(error?.message || error);
|
|
if (message.includes("HTTP 404")) {
|
|
log(`ImageMagick asset not found (${candidate}); trying next candidate.`);
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!assetName || !appImagePath) {
|
|
const reason = lastDownloadError?.message || lastDownloadError || "no candidate succeeded";
|
|
fail(`Could not fetch Linux ImageMagick AppImage: ${reason}`);
|
|
}
|
|
|
|
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
fs.mkdirSync(path.join(tempTarget, "bin"), { recursive: true });
|
|
|
|
const runtimeAppImage = path.join(tempTarget, assetName);
|
|
fs.copyFileSync(appImagePath, runtimeAppImage);
|
|
fs.chmodSync(runtimeAppImage, 0o755);
|
|
|
|
const wrapperPath = path.join(tempTarget, "bin", "magick");
|
|
fs.writeFileSync(
|
|
wrapperPath,
|
|
[
|
|
"#!/usr/bin/env sh",
|
|
"set -eu",
|
|
'DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"',
|
|
"export APPIMAGE_EXTRACT_AND_RUN=${APPIMAGE_EXTRACT_AND_RUN:-1}",
|
|
'exec "$DIR/' + assetName + '" "$@"',
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
fs.chmodSync(wrapperPath, 0o755);
|
|
writeManifest(tempTarget, {
|
|
kind: "linux-appimage",
|
|
binary: "bin/magick",
|
|
appImage: assetName,
|
|
source: downloadUrl(assetName),
|
|
});
|
|
|
|
if (!validateRuntime(tempTarget)) {
|
|
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
|
}
|
|
|
|
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
|
fs.renameSync(tempTarget, TARGET_DIR);
|
|
log(`Prepared ${path.join(TARGET_DIR, "bin", "magick")}`);
|
|
}
|
|
|
|
function resolveCommandPath(command) {
|
|
if (path.isAbsolute(command)) {
|
|
return fs.existsSync(command) ? command : null;
|
|
}
|
|
const result = capture("which", [command]);
|
|
const resolved = (result.stdout || "").trim().split(/\r?\n/).filter(Boolean)[0];
|
|
return result.status === 0 && resolved ? resolved : null;
|
|
}
|
|
|
|
function resolveMacPrefixFromMagickBinary(magickPath) {
|
|
if (!magickPath || !fs.existsSync(magickPath)) {
|
|
return null;
|
|
}
|
|
const realMagick = fs.realpathSync(magickPath);
|
|
const binDir = path.dirname(realMagick);
|
|
const prefix = path.dirname(binDir);
|
|
return fs.existsSync(path.join(prefix, "bin", "magick")) ? prefix : null;
|
|
}
|
|
|
|
function listBrewCandidates() {
|
|
return [resolveCommandPath("brew"), "/opt/homebrew/bin/brew", "/usr/local/bin/brew"]
|
|
.filter(Boolean)
|
|
.filter((candidate, index, all) => all.indexOf(candidate) === index)
|
|
.filter((candidate) => fs.existsSync(candidate));
|
|
}
|
|
|
|
function resolveMacPrefixFromBrew() {
|
|
const brewCandidates = listBrewCandidates();
|
|
const formulas = ["imagemagick", "imagemagick@6"];
|
|
|
|
for (const brew of brewCandidates) {
|
|
for (const formula of formulas) {
|
|
const result = capture(brew, ["--prefix", formula]);
|
|
const prefix = (result.stdout || "").trim();
|
|
if (result.status !== 0 || !prefix) {
|
|
continue;
|
|
}
|
|
if (fs.existsSync(path.join(prefix, "bin", "magick"))) {
|
|
return prefix;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function ensureMacImageMagickWithBrew() {
|
|
for (const brew of listBrewCandidates()) {
|
|
log(`ImageMagick not found; trying to install with Homebrew (${brew} install imagemagick).`);
|
|
const install = spawnSync(brew, ["install", "imagemagick"], {
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
});
|
|
if (install.status === 0) {
|
|
return true;
|
|
}
|
|
const reason = install.error?.message || `exit ${install.status}`;
|
|
log(`Homebrew installation attempt failed via ${brew}: ${reason}`);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function resolveMacSourcePrefix() {
|
|
const configured = process.env.IMAGEMAGICK_VENDOR_DIR?.trim();
|
|
if (configured) {
|
|
return path.resolve(configured);
|
|
}
|
|
|
|
const pathMagickPrefix = resolveMacPrefixFromMagickBinary(resolveCommandPath("magick"));
|
|
if (pathMagickPrefix) {
|
|
return pathMagickPrefix;
|
|
}
|
|
|
|
for (const magickPath of ["/opt/homebrew/bin/magick", "/usr/local/bin/magick", "/opt/local/bin/magick"]) {
|
|
const prefix = resolveMacPrefixFromMagickBinary(magickPath);
|
|
if (prefix) {
|
|
return prefix;
|
|
}
|
|
}
|
|
|
|
const brewPrefix = resolveMacPrefixFromBrew();
|
|
if (brewPrefix) {
|
|
return brewPrefix;
|
|
}
|
|
|
|
for (const optPrefix of ["/opt/homebrew/opt/imagemagick", "/usr/local/opt/imagemagick"]) {
|
|
if (fs.existsSync(path.join(optPrefix, "bin", "magick"))) {
|
|
return optPrefix;
|
|
}
|
|
}
|
|
|
|
if (ensureMacImageMagickWithBrew()) {
|
|
const installedPrefix = resolveMacPrefixFromMagickBinary(resolveCommandPath("magick"))
|
|
|| resolveMacPrefixFromBrew();
|
|
if (installedPrefix) {
|
|
return installedPrefix;
|
|
}
|
|
log("Homebrew install succeeded but ImageMagick prefix was not auto-detected; continuing with fallback checks.");
|
|
}
|
|
|
|
fail("Could not find a macOS ImageMagick runtime to vendor. Install ImageMagick (brew install imagemagick) and rerun, or set IMAGEMAGICK_VENDOR_DIR.");
|
|
}
|
|
|
|
function parseOtoolDeps(filePath) {
|
|
const result = capture("otool", ["-L", filePath]);
|
|
if (result.status !== 0) {
|
|
fail(`otool -L failed for ${filePath}: ${result.stderr || result.status}`);
|
|
}
|
|
return (result.stdout || "")
|
|
.split(/\r?\n/)
|
|
.slice(1)
|
|
.map((line) => line.trim().split(/\s+/)[0])
|
|
.filter(Boolean)
|
|
.filter((dep) => dep.startsWith("/"))
|
|
.filter((dep) => !dep.startsWith("/System/Library/") && !dep.startsWith("/usr/lib/"));
|
|
}
|
|
|
|
function isMachOFile(filePath) {
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
return false;
|
|
}
|
|
const result = capture("file", [filePath]);
|
|
return result.status === 0 && /Mach-O/.test(result.stdout || "");
|
|
}
|
|
|
|
function walkFiles(rootDir) {
|
|
const stack = [rootDir];
|
|
const files = [];
|
|
while (stack.length) {
|
|
const current = stack.pop();
|
|
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(fullPath);
|
|
} else if (entry.isFile()) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function relinkMacDylibs(targetDir, mainExecutable) {
|
|
for (const tool of ["otool", "install_name_tool", "file"]) {
|
|
if (!resolveCommandPath(tool)) {
|
|
fail(`macOS runtime vendoring requires ${tool}.`);
|
|
}
|
|
}
|
|
|
|
const libDir = path.join(targetDir, "lib");
|
|
fs.mkdirSync(libDir, { recursive: true });
|
|
|
|
const queue = [mainExecutable];
|
|
const visited = new Set();
|
|
const copiedBySource = new Map();
|
|
|
|
while (queue.length) {
|
|
const current = queue.shift();
|
|
if (visited.has(current) || !fs.existsSync(current) || !isMachOFile(current)) {
|
|
continue;
|
|
}
|
|
visited.add(current);
|
|
|
|
const deps = parseOtoolDeps(current);
|
|
for (const dep of deps) {
|
|
const depBase = path.basename(dep);
|
|
let vendored = copiedBySource.get(dep);
|
|
if (!vendored) {
|
|
vendored = path.join(libDir, depBase);
|
|
if (!fs.existsSync(vendored)) {
|
|
fs.copyFileSync(dep, vendored);
|
|
fs.chmodSync(vendored, 0o755);
|
|
}
|
|
copiedBySource.set(dep, vendored);
|
|
queue.push(vendored);
|
|
}
|
|
|
|
const replacement = current === mainExecutable
|
|
? `@executable_path/../lib/${depBase}`
|
|
: `@loader_path/${depBase}`;
|
|
run("install_name_tool", ["-change", dep, replacement, current]);
|
|
}
|
|
|
|
if (current !== mainExecutable) {
|
|
run("install_name_tool", ["-id", `@loader_path/${path.basename(current)}`, current]);
|
|
}
|
|
}
|
|
}
|
|
|
|
function adHocSignMacRuntime(targetDir, mainExecutable) {
|
|
if (!resolveCommandPath("codesign")) {
|
|
fail("macOS runtime vendoring requires codesign.");
|
|
}
|
|
|
|
const machOFiles = walkFiles(targetDir).filter(isMachOFile);
|
|
const signOrder = machOFiles
|
|
.filter((filePath) => filePath !== mainExecutable)
|
|
.concat(machOFiles.includes(mainExecutable) ? [mainExecutable] : []);
|
|
|
|
log(`Ad-hoc signing ${signOrder.length} Mach-O files for macOS runtime.`);
|
|
for (const filePath of signOrder) {
|
|
run("codesign", ["--force", "--sign", "-", "--timestamp=none", filePath]);
|
|
}
|
|
}
|
|
|
|
async function prepareMacOS() {
|
|
const sourcePrefix = resolveMacSourcePrefix();
|
|
const sourceMagick = path.join(sourcePrefix, "bin", "magick");
|
|
if (!fs.existsSync(sourceMagick)) {
|
|
fail(`macOS ImageMagick prefix does not contain bin/magick: ${sourcePrefix}`);
|
|
}
|
|
|
|
const tempTarget = `${TARGET_DIR}.tmp`;
|
|
fs.rmSync(tempTarget, { recursive: true, force: true });
|
|
fs.mkdirSync(tempTarget, { recursive: true });
|
|
fs.cpSync(sourcePrefix, tempTarget, {
|
|
recursive: true,
|
|
dereference: true,
|
|
filter(source) {
|
|
const base = path.basename(source);
|
|
return base !== ".brew" && base !== "INSTALL_RECEIPT.json";
|
|
},
|
|
});
|
|
|
|
const targetMagick = path.join(tempTarget, "bin", "magick");
|
|
fs.chmodSync(targetMagick, 0o755);
|
|
relinkMacDylibs(tempTarget, targetMagick);
|
|
adHocSignMacRuntime(tempTarget, targetMagick);
|
|
writeManifest(tempTarget, {
|
|
kind: "macos-vendored",
|
|
binary: "bin/magick",
|
|
source: sourcePrefix,
|
|
});
|
|
|
|
if (!validateRuntime(tempTarget)) {
|
|
fail(`Prepared runtime failed validation at ${tempTarget}`);
|
|
}
|
|
|
|
fs.rmSync(TARGET_DIR, { recursive: true, force: true });
|
|
fs.renameSync(tempTarget, TARGET_DIR);
|
|
log(`Prepared ${path.join(TARGET_DIR, "bin", "magick")}`);
|
|
}
|
|
|
|
async function main() {
|
|
const existing = validateRuntime(TARGET_DIR);
|
|
if (existing) {
|
|
log(`Existing runtime OK: ${existing.binaryPath}`);
|
|
return;
|
|
}
|
|
|
|
if (PLATFORM === "win32") {
|
|
await prepareWindows();
|
|
return;
|
|
}
|
|
if (PLATFORM === "linux") {
|
|
await prepareLinux();
|
|
return;
|
|
}
|
|
if (PLATFORM === "darwin") {
|
|
await prepareMacOS();
|
|
return;
|
|
}
|
|
|
|
fail(`Unsupported platform for bundled ImageMagick: ${PLATFORM}-${ARCH}`);
|
|
}
|
|
|
|
main().catch((error) => fail(error?.stack || error?.message || String(error)));
|