Clean and verify Galaxy before local macOS installation
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { render, Text, Box } from "ink";
|
import { render, Text, Box } from "ink";
|
||||||
import Spinner from "ink-spinner";
|
import Spinner from "ink-spinner";
|
||||||
import { spawn, execFileSync, execSync } from "child_process";
|
import { spawn, execFileSync } from "child_process";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { prepareGalaxyBuild } from "../../lib/prepare-galaxy-build.mjs";
|
import { prepareGalaxyBuild } from "../../lib/prepare-galaxy-build.mjs";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -27,14 +28,15 @@ const MAX_LOG_LINES = 15;
|
|||||||
|
|
||||||
function runCommandStreaming(
|
function runCommandStreaming(
|
||||||
cmd: string,
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
cwd: string,
|
cwd: string,
|
||||||
onLog: (line: string) => void,
|
onLog: (line: string) => void,
|
||||||
interactive = false
|
interactive = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(cmd, {
|
const child = spawn(cmd, args, {
|
||||||
cwd,
|
cwd,
|
||||||
shell: true,
|
shell: false,
|
||||||
stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
|
stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,13 +69,29 @@ function runCommandStreaming(
|
|||||||
stdout.flush();
|
stdout.flush();
|
||||||
stderr.flush();
|
stderr.flush();
|
||||||
if (code === 0) resolve();
|
if (code === 0) resolve();
|
||||||
else reject(new Error(`Command failed with exit code ${code}`));
|
else reject(new Error(`${cmd} failed with exit code ${code}`));
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", reject);
|
child.on("error", reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeBundleArtifacts(directory: string): void {
|
||||||
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const entryPath = path.join(directory, entry.name);
|
||||||
|
if (
|
||||||
|
entry.name === "__MACOSX" ||
|
||||||
|
entry.name === ".DS_Store" ||
|
||||||
|
entry.name.startsWith("._")
|
||||||
|
) {
|
||||||
|
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
// Do not follow bundle symlinks outside the private installation copy.
|
||||||
|
removeBundleArtifacts(entryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isAppRunning(processName: string): boolean {
|
function isAppRunning(processName: string): boolean {
|
||||||
try {
|
try {
|
||||||
execFileSync("pgrep", ["-x", processName], { stdio: "ignore" });
|
execFileSync("pgrep", ["-x", processName], { stdio: "ignore" });
|
||||||
@@ -168,7 +186,8 @@ function App() {
|
|||||||
{ label: "Check working directory", status: "pending" },
|
{ label: "Check working directory", status: "pending" },
|
||||||
{ label: "Pull current branch", status: "pending" },
|
{ label: "Pull current branch", status: "pending" },
|
||||||
{ label: "Bundle Galaxy", status: "pending" },
|
{ label: "Bundle Galaxy", status: "pending" },
|
||||||
{ label: "Copy to /Applications", status: "pending" },
|
{ label: "Clean and sign Galaxy", status: "pending" },
|
||||||
|
{ label: "Verify and install in /Applications", status: "pending" },
|
||||||
{ label: "Launch Galaxy", status: "pending" },
|
{ label: "Launch Galaxy", status: "pending" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -196,14 +215,34 @@ function App() {
|
|||||||
prepared = await prepareGalaxyBuild(WORKSPACE_ROOT, { updateStep, appendLog });
|
prepared = await prepareGalaxyBuild(WORKSPACE_ROOT, { updateStep, appendLog });
|
||||||
const { appPath } = prepared;
|
const { appPath } = prepared;
|
||||||
|
|
||||||
// ─── Step 3: Copy to /Applications ─────────────────────────────
|
// ─── Step 3: Clean and sign the private bundle copy ────────────
|
||||||
updateStep(3, { status: "running" });
|
updateStep(3, { status: "running" });
|
||||||
|
|
||||||
if (!fs.existsSync(appPath)) {
|
if (!fs.existsSync(appPath)) {
|
||||||
throw new Error(`Built app not found at ${appPath}`);
|
throw new Error(`Built app not found at ${appPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await stopApp("Galaxy", (line) => appendLog(3, line));
|
appendLog(3, "Removing macOS archive artifacts...");
|
||||||
|
removeBundleArtifacts(appPath);
|
||||||
|
appendLog(3, "Clearing extended attributes, including quarantine...");
|
||||||
|
await runCommandStreaming(
|
||||||
|
"/usr/bin/xattr", ["-cr", appPath],
|
||||||
|
WORKSPACE_ROOT, (line) => appendLog(3, line)
|
||||||
|
);
|
||||||
|
appendLog(3, "Applying an ad-hoc code signature...");
|
||||||
|
await runCommandStreaming(
|
||||||
|
"/usr/bin/codesign", ["--force", "--deep", "--sign", "-", appPath],
|
||||||
|
WORKSPACE_ROOT, (line) => appendLog(3, line)
|
||||||
|
);
|
||||||
|
await runCommandStreaming(
|
||||||
|
"/usr/bin/codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath],
|
||||||
|
WORKSPACE_ROOT, (line) => appendLog(3, line)
|
||||||
|
);
|
||||||
|
updateStep(3, { status: "done" });
|
||||||
|
|
||||||
|
// ─── Step 4: Verify the staged copy before replacing the app ───
|
||||||
|
updateStep(4, { status: "running" });
|
||||||
|
const stagedPath = path.join(path.dirname(destPath), `.Galaxy.app.${randomUUID()}.new`);
|
||||||
|
|
||||||
let needsAdministrator = false;
|
let needsAdministrator = false;
|
||||||
try {
|
try {
|
||||||
@@ -212,39 +251,53 @@ function App() {
|
|||||||
} catch {
|
} catch {
|
||||||
needsAdministrator = true;
|
needsAdministrator = true;
|
||||||
}
|
}
|
||||||
const installPrefix = needsAdministrator ? "sudo -n " : "";
|
|
||||||
if (needsAdministrator) {
|
if (needsAdministrator) {
|
||||||
appendLog(3, "Administrator password required to install in /Applications");
|
appendLog(4, "Administrator password required to install in /Applications");
|
||||||
await runCommandStreaming(
|
await runCommandStreaming(
|
||||||
"sudo -v",
|
"/usr/bin/sudo", ["-v"],
|
||||||
WORKSPACE_ROOT,
|
WORKSPACE_ROOT,
|
||||||
(line) => appendLog(3, line),
|
(line) => appendLog(4, line),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(destPath)) {
|
const runInstallCommand = (command: string, args: string[]) =>
|
||||||
appendLog(3, `Removing existing ${destPath}`);
|
runCommandStreaming(
|
||||||
await runCommandStreaming(
|
needsAdministrator ? "/usr/bin/sudo" : command,
|
||||||
`${installPrefix}rm -rf "${destPath}"`,
|
needsAdministrator ? ["-n", command, ...args] : args,
|
||||||
WORKSPACE_ROOT,
|
WORKSPACE_ROOT,
|
||||||
(line) => appendLog(3, line)
|
(line) => appendLog(4, line)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
appendLog(4, `Staging ${appPath} → ${stagedPath}`);
|
||||||
|
await runInstallCommand("/usr/bin/ditto", [appPath, stagedPath]);
|
||||||
|
await runCommandStreaming(
|
||||||
|
"/usr/bin/codesign", ["--verify", "--deep", "--strict", "--verbose=2", stagedPath],
|
||||||
|
WORKSPACE_ROOT, (line) => appendLog(4, line)
|
||||||
|
);
|
||||||
|
|
||||||
|
await stopApp("galaxy-oss", (line) => appendLog(4, line));
|
||||||
|
if (fs.existsSync(destPath)) {
|
||||||
|
appendLog(4, `Removing existing ${destPath}`);
|
||||||
|
await runInstallCommand("/bin/rm", ["-rf", destPath]);
|
||||||
|
}
|
||||||
|
await runInstallCommand("/bin/mv", [stagedPath, destPath]);
|
||||||
|
} finally {
|
||||||
|
if (fs.existsSync(stagedPath)) {
|
||||||
|
await runInstallCommand("/bin/rm", ["-rf", stagedPath]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendLog(3, `Copying ${appPath} → ${destPath}`);
|
updateStep(4, { status: "done", detail: destPath });
|
||||||
|
|
||||||
|
// ─── Step 5: Launch ────────────────────────────────────────────
|
||||||
|
updateStep(5, { status: "running" });
|
||||||
await runCommandStreaming(
|
await runCommandStreaming(
|
||||||
`${installPrefix}ditto "${appPath}" "${destPath}"`,
|
"/usr/bin/open", [destPath],
|
||||||
WORKSPACE_ROOT,
|
WORKSPACE_ROOT, (line) => appendLog(5, line)
|
||||||
(line) => appendLog(3, line)
|
|
||||||
);
|
);
|
||||||
|
updateStep(5, { status: "done" });
|
||||||
updateStep(3, { status: "done", detail: destPath });
|
|
||||||
|
|
||||||
// ─── Step 4: Launch ────────────────────────────────────────────
|
|
||||||
updateStep(4, { status: "running" });
|
|
||||||
execSync(`open "${destPath}"`, { stdio: "ignore" });
|
|
||||||
updateStep(4, { status: "done" });
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
setSteps((prev) =>
|
setSteps((prev) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user