Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
716b302374 | ||
|
|
20d6bf7fdc |
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
|
||||
/// Resolves Argyll sidecar binaries (docs/04 §0.1 `resolve_binary`).
|
||||
///
|
||||
/// Order:
|
||||
/// 1. Settings `argyll_binary_dir` override — only if `<dir>/<name>`
|
||||
/// exists there.
|
||||
/// 2. Bundled `<bundle>/Resources/Argyll/<platform>/<name>`.
|
||||
/// On macOS, `macos-universal` wins whenever it contains the `instlist`
|
||||
/// marker; otherwise `macos-arm64` / `macos-x86_64` by host arch.
|
||||
/// 3. If nothing exists the *constructed* bundled path is still returned
|
||||
/// — a missing binary surfaces later as `process:error` on spawn,
|
||||
/// matching v1 semantics.
|
||||
public struct BinaryResolver: Sendable {
|
||||
|
||||
/// Root that contains the platform dirs — `Bundle.resource/Argyll` in
|
||||
/// the app, a fixture dir in tests.
|
||||
public let bundledRoot: URL
|
||||
/// `settings.argyll_binary_dir`, already expanded to a URL.
|
||||
public let overrideDir: URL?
|
||||
/// Host architecture directory names, universal preferred.
|
||||
public let archDirs: [String]
|
||||
|
||||
public init(
|
||||
bundledRoot: URL = AppPaths.bundledArgyllDir,
|
||||
overrideDir: URL? = nil,
|
||||
archDirs: [String]? = nil
|
||||
) {
|
||||
self.bundledRoot = bundledRoot
|
||||
self.overrideDir = overrideDir
|
||||
#if arch(arm64)
|
||||
let fallback = ["macos-arm64", "macos-aarch64"]
|
||||
#else
|
||||
let fallback = ["macos-x86_64"]
|
||||
#endif
|
||||
self.archDirs = archDirs ?? ["macos-universal"] + fallback
|
||||
}
|
||||
|
||||
/// Marker used to decide whether `macos-universal` is usable.
|
||||
public static let markerBinary = "instlist"
|
||||
|
||||
/// Resolves a tool name to an absolute URL (never throws — see type
|
||||
/// docs). `name` is the bare tool name, e.g. `"targen"`.
|
||||
public func resolve(_ name: String) -> URL {
|
||||
let fm = FileManager.default
|
||||
|
||||
if let dir = overrideDir {
|
||||
let candidate = dir.appendingPathComponent(name)
|
||||
if fm.fileExists(atPath: candidate.path) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return bundledRoot
|
||||
.appendingPathComponent(platformDir(), isDirectory: true)
|
||||
.appendingPathComponent(name, isDirectory: false)
|
||||
}
|
||||
|
||||
/// The bundled platform directory that resolution will use.
|
||||
public func platformDir() -> String {
|
||||
let fm = FileManager.default
|
||||
let universal = bundledRoot.appendingPathComponent("macos-universal")
|
||||
if fm.fileExists(
|
||||
atPath: universal.appendingPathComponent(Self.markerBinary).path
|
||||
) {
|
||||
return "macos-universal"
|
||||
}
|
||||
for dir in archDirs where dir != "macos-universal" {
|
||||
if fm.fileExists(
|
||||
atPath: bundledRoot
|
||||
.appendingPathComponent(dir)
|
||||
.appendingPathComponent(Self.markerBinary).path
|
||||
) {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
// Nothing present — still return the preferred dir so the error
|
||||
// message points at where the user should drop binaries.
|
||||
return archDirs.first ?? "macos-universal"
|
||||
}
|
||||
|
||||
/// Bundled mock tool (tracked in git under `Resources/Argyll/mocks/`).
|
||||
public func mock(_ name: String) -> URL {
|
||||
bundledRoot
|
||||
.appendingPathComponent("mocks", isDirectory: true)
|
||||
.appendingPathComponent("\(name).mock", isDirectory: false)
|
||||
}
|
||||
|
||||
/// Bundled reference gamut (`Resources/Argyll/reference_gamuts/`).
|
||||
public func referenceGamut(_ name: String) -> URL {
|
||||
bundledRoot
|
||||
.appendingPathComponent("reference_gamuts", isDirectory: true)
|
||||
.appendingPathComponent(name, isDirectory: false)
|
||||
}
|
||||
|
||||
/// Whether the resolved path exists and is executable.
|
||||
public func exists(_ url: URL) -> Bool {
|
||||
FileManager.default.isExecutableFile(atPath: url.path)
|
||||
}
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# Mock script for chartread -u
|
||||
# This script simulates the behaviour of chartread for testing purposes.
|
||||
|
||||
# Check for --xy argument or MOCK_XY_TABLE environment variable
|
||||
IS_XY=0
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--xy" ]; then
|
||||
IS_XY=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$IS_XY" = "1" ] || [ "${MOCK_XY_TABLE}" = "1" ]; then
|
||||
echo "Place instrument on calibration tile and hit [Space] to calibrate."
|
||||
read -r _calib
|
||||
echo "Calibration successful."
|
||||
|
||||
echo "Please place sheet 1 of 1 on the table"
|
||||
echo "hit return to continue, Esc or 'q' to give up"
|
||||
read -r _sheet1
|
||||
|
||||
echo "locate patch A1 with the sight,"
|
||||
echo "then hit return to continue"
|
||||
read -r _fid1
|
||||
|
||||
echo "locate patch B24 with the sight,"
|
||||
echo "then hit return to continue"
|
||||
read -r _fid2
|
||||
|
||||
echo "Reading sheet 1..."
|
||||
sleep 0.5
|
||||
|
||||
# Emit mock JSON for strip A
|
||||
cat << 'EOF'
|
||||
ROW_COLORS_JSON: {"event": "row_complete", "row_id": "A", "row_index": 0, "total_rows": 2, "patch_count": 3, "patches": [{"id": "1", "loc": "A1", "is_pad": false, "device": [0.0, 50.0, 100.0], "expected": {"XYZ": [18.4210, 20.1234, 15.6789], "Lab": [51.98, -8.45, 12.32]}, "measured": {"XYZ": [18.5120, 20.0451, 15.7100], "Lab": [51.89, -8.31, 12.15]}}, {"id": "2", "loc": "A2", "is_pad": false, "device": [10.0, 60.0, 90.0], "expcted": {"Lab": [60.0, 10.0, -20.0]}, "measured": {"Lab": [60.1, 10.5, -19.5]}}, {"id": "3", "loc": "A3", "is_pad": true, "device": [100.0, 100.0, 100.0]}]}
|
||||
EOF
|
||||
|
||||
# Emit mock JSON for strip B
|
||||
cat << 'EOF'
|
||||
ROW_COLORS_JSON: {"event": "row_complete", "row_id": "B", "row_index": 1, "total_rows": 2, "patch_count": 2, "patches": [{"id": "4", "loc": "B1", "is_pad": false, "device": [100.0, 0.0, 0.0], "expected": {"Lab": [40.0, 40.0, 40.0]}, "measured": {"Lab": [38.0, 41.0, 39.0]}}, {"id": "5", "loc": "B2", "is_pad": false, "device": [0.0, 100.0, 0.0], "expcted": {"Lab": [80.0, -50.0, 50.0]}, "measured": {"Lab": [79.0, -49.0, 51.0]}}]}
|
||||
EOF
|
||||
|
||||
echo "Sheet 1 of 1 read OK"
|
||||
echo "Please remove last sheet from table"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handheld / strip reader simulation
|
||||
echo "Place instrument on calibration tile and hit [Space] to calibrate."
|
||||
|
||||
# We don't really wait for input, just wait 1 second
|
||||
sleep 1
|
||||
echo "Calibration successful."
|
||||
echo "Hit [Space] to read strip A (or 's' to skip)."
|
||||
|
||||
sleep 1
|
||||
echo "Reading strip A..."
|
||||
|
||||
# Emit mock JSON for strip A
|
||||
cat << 'EOF'
|
||||
ROW_COLORS_JSON: {"event": "row_complete", "row_id": "A", "row_index": 0, "total_rows": 2, "patch_count": 3, "patches": [{"id": "1", "loc": "A1", "is_pad": false, "device": [0.0, 50.0, 100.0], "expected": {"XYZ": [18.4210, 20.1234, 15.6789], "Lab": [51.98, -8.45, 12.32]}, "measured": {"XYZ": [18.5120, 20.0451, 15.7100], "Lab": [51.89, -8.31, 12.15]}}, {"id": "2", "loc": "A2", "is_pad": false, "device": [10.0, 60.0, 90.0], "expcted": {"Lab": [60.0, 10.0, -20.0]}, "measured": {"Lab": [60.1, 10.5, -19.5]}}, {"id": "3", "loc": "A3", "is_pad": true, "device": [100.0, 100.0, 100.0]}]}
|
||||
EOF
|
||||
|
||||
echo "Hit [Space] to read strip B (or 's' to skip)."
|
||||
sleep 1
|
||||
echo "Reading strip B..."
|
||||
|
||||
# Emit mock JSON for strip B
|
||||
cat << 'EOF'
|
||||
ROW_COLORS_JSON: {"event": "row_complete", "row_id": "B", "row_index": 1, "total_rows": 2, "patch_count": 2, "patches": [{"id": "4", "loc": "B1", "is_pad": false, "device": [100.0, 0.0, 0.0], "expected": {"Lab": [40.0, 40.0, 40.0]}, "measured": {"Lab": [38.0, 41.0, 39.0]}}, {"id": "5", "loc": "B2", "is_pad": false, "device": [0.0, 100.0, 0.0], "expected": {"Lab": [80.0, -50.0, 50.0]}, "measured": {"Lab": [79.0, -49.0, 51.0]}}]}
|
||||
EOF
|
||||
|
||||
echo "Ready to read... done."
|
||||
exit 0
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Mock script for colprof
|
||||
# Simulates colprof execution and outputs progress log
|
||||
|
||||
basename="$1"
|
||||
# Find last argument if -D or other flags are used
|
||||
for arg in "$@"; do
|
||||
basename="$arg"
|
||||
done
|
||||
|
||||
echo "colprof: Starting profile calculation for $basename"
|
||||
sleep 1
|
||||
echo "Gamut mapping calculation..."
|
||||
sleep 1
|
||||
echo "Fitting cLUT grid points..."
|
||||
sleep 1
|
||||
echo "Writing ICC profile $basename.icc..."
|
||||
touch "$basename.icc"
|
||||
echo "Done."
|
||||
exit 0
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Mock script for profcheck
|
||||
# Simulates real ArgyllCMS profcheck -v -k -s -u output
|
||||
|
||||
echo "profcheck: Checking profile accuracy..."
|
||||
echo "No of test patches = 52"
|
||||
sleep 1
|
||||
cat << 'EOF'
|
||||
{"event": "report", "peak_de2000": 2.41, "avg_de2000": 0.85, "rms": 1.02}
|
||||
EOF
|
||||
echo "Profile check complete, errors(CIEDE2000): max. = 2.41, avg. = 0.85, RMS = 1.02"
|
||||
exit 0
|
||||
@@ -0,0 +1,16 @@
|
||||
CGATS.17
|
||||
NUMBER_OF_FIELDS 4
|
||||
BEGIN_DATA_FORMAT
|
||||
INDEX LAB_L LAB_A LAB_B
|
||||
END_DATA_FORMAT
|
||||
NUMBER_OF_SETS 8
|
||||
BEGIN_DATA
|
||||
0 0.0 0.0 0.0
|
||||
1 100.0 0.0 0.0
|
||||
2 53.2 80.1 67.2
|
||||
3 87.7 -86.2 83.2
|
||||
4 97.1 -21.6 94.5
|
||||
5 32.3 79.2 -107.9
|
||||
6 60.3 98.2 -60.8
|
||||
7 91.1 -48.1 -14.1
|
||||
END_DATA
|
||||
@@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import ICCeryCore
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
@@ -19,15 +20,24 @@ struct ICCeryApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
/// AppDelegate: quit when the single window closes, and give later
|
||||
/// milestones a hook to `killAll` Argyll children before teardown
|
||||
/// (#147/#149 — wired once ProcessManager exists in #2).
|
||||
/// AppDelegate: quit when the single window closes, and `killAll` Argyll
|
||||
/// children before teardown (#147/#149). Termination is deferred until
|
||||
/// `killAll` has signaled every child so `chartread` can park an XY head
|
||||
/// when the UI already sent `q\n`.
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var terminationRequested = false
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
// Issue #2+: ProcessManager.shared.killAll()
|
||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
guard !terminationRequested else { return .terminateNow }
|
||||
terminationRequested = true
|
||||
Task {
|
||||
await ProcessManager.shared.killAll()
|
||||
NSApplication.shared.reply(toApplicationShouldTerminate: true)
|
||||
}
|
||||
return .terminateLater
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import ICCeryCore
|
||||
|
||||
@Suite("BinaryResolver")
|
||||
struct BinaryResolverTests {
|
||||
|
||||
private func makeTree(_ body: (URL) throws -> Void) throws -> URL {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("iccery-resolver-\(UUID().uuidString)")
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
try body(root)
|
||||
return root
|
||||
}
|
||||
|
||||
private func touch(_ url: URL, executable: Bool = true) throws {
|
||||
FileManager.default.createFile(atPath: url.path, contents: Data())
|
||||
if executable {
|
||||
try FileManager.default.setAttributes(
|
||||
[.posixPermissions: 0o755], ofItemAtPath: url.path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func overrideDirWinsWhenFileExists() throws {
|
||||
let override = try makeTree { root in
|
||||
try touch(root.appendingPathComponent("targen"))
|
||||
}
|
||||
let bundled = try makeTree { _ in }
|
||||
let r = BinaryResolver(bundledRoot: bundled, overrideDir: override)
|
||||
#expect(r.resolve("targen") == override.appendingPathComponent("targen"))
|
||||
}
|
||||
|
||||
@Test func overrideFallsThroughWhenMissing() throws {
|
||||
let override = try makeTree { _ in }
|
||||
let bundled = try makeTree { root in
|
||||
let dir = root.appendingPathComponent("macos-universal")
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
try touch(dir.appendingPathComponent("instlist"))
|
||||
}
|
||||
let r = BinaryResolver(bundledRoot: bundled, overrideDir: override)
|
||||
#expect(r.resolve("targen").path.contains("macos-universal/targen"))
|
||||
}
|
||||
|
||||
@Test func universalPreferredWhenMarkerPresent() throws {
|
||||
let bundled = try makeTree { root in
|
||||
for dir in ["macos-universal", "macos-x86_64"] {
|
||||
let d = root.appendingPathComponent(dir)
|
||||
try FileManager.default.createDirectory(at: d, withIntermediateDirectories: true)
|
||||
try touch(d.appendingPathComponent("instlist"))
|
||||
}
|
||||
}
|
||||
let r = BinaryResolver(bundledRoot: bundled)
|
||||
#expect(r.platformDir() == "macos-universal")
|
||||
}
|
||||
|
||||
@Test func fallsBackToArchDir() throws {
|
||||
let bundled = try makeTree { root in
|
||||
let d = root.appendingPathComponent("macos-x86_64")
|
||||
try FileManager.default.createDirectory(at: d, withIntermediateDirectories: true)
|
||||
try touch(d.appendingPathComponent("instlist"))
|
||||
}
|
||||
let r = BinaryResolver(
|
||||
bundledRoot: bundled,
|
||||
archDirs: ["macos-universal", "macos-x86_64"]
|
||||
)
|
||||
#expect(r.platformDir() == "macos-x86_64")
|
||||
}
|
||||
|
||||
@Test func missingEverythingReturnsConstructedPath() throws {
|
||||
let bundled = try makeTree { _ in }
|
||||
let r = BinaryResolver(bundledRoot: bundled)
|
||||
// v1 semantic: path is returned; spawn surfaces the error.
|
||||
#expect(r.resolve("targen").path.hasSuffix("macos-universal/targen"))
|
||||
#expect(!r.exists(r.resolve("targen")))
|
||||
}
|
||||
|
||||
@Test func mockAndGamutPaths() throws {
|
||||
let r = BinaryResolver(bundledRoot: URL(fileURLWithPath: "/x"))
|
||||
#expect(r.mock("chartread").path == "/x/mocks/chartread.mock")
|
||||
#expect(r.referenceGamut("sRGB.gam").path == "/x/reference_gamuts/sRGB.gam")
|
||||
}
|
||||
}
|
||||
+16
@@ -19,9 +19,25 @@ targets:
|
||||
- path: Resources
|
||||
excludes:
|
||||
- ICCery.entitlements
|
||||
- Argyll
|
||||
- path: Resources/Argyll
|
||||
type: folder
|
||||
dependencies:
|
||||
- package: ICCeryCore
|
||||
product: ICCeryCore
|
||||
postBuildScripts:
|
||||
- name: Copy Argyll sidecars
|
||||
script: |
|
||||
set -e
|
||||
SRC="${SRCROOT}/Vendor/Argyll"
|
||||
DEST="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Argyll"
|
||||
if [ -d "$SRC" ]; then
|
||||
mkdir -p "$DEST"
|
||||
rsync -a "$SRC/" "$DEST/"
|
||||
else
|
||||
echo "note: Vendor/Argyll absent — run scripts/fetch-argyll.sh"
|
||||
fi
|
||||
basedOnDependencyAnalysis: false
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.gronod.iccery2
|
||||
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/sh
|
||||
# scripts/fetch-argyll.sh
|
||||
#
|
||||
# Downloads the Gronod ArgyllCMS fork release (macOS universal binaries)
|
||||
# into Vendor/Argyll/. POSIX sh + curl + tar — no Node dependency.
|
||||
#
|
||||
# Env overrides (parity with v1 fetch-argyll.mjs):
|
||||
# ARGYLL_SERVER_URL default https://git.i3omb.com
|
||||
# ARGYLL_REPO default gronod/argyllcms
|
||||
# ARGYLL_RELEASE_TAG default: latest release
|
||||
# GITEA_TOKEN optional, for private repos
|
||||
#
|
||||
# Layout produced (docs/04 §0.6, docs/02 §Sidecar layout):
|
||||
# Vendor/Argyll/macos-universal/<tools> # marker binary: instlist
|
||||
# Mocks and reference_gamuts are tracked under Resources/Argyll/ —
|
||||
# they ship in git, not in the release tarball.
|
||||
|
||||
set -eu
|
||||
|
||||
SERVER="${ARGYLL_SERVER_URL:-https://git.i3omb.com}"
|
||||
REPO="${ARGYLL_REPO:-gronod/argyllcms}"
|
||||
TAG="${ARGYLL_RELEASE_TAG:-}"
|
||||
SUFFIX="_macOS_universal_bin.tgz"
|
||||
PLATFORM_DIR="macos-universal"
|
||||
MARKER="instlist"
|
||||
|
||||
ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
DEST="$ROOT/Vendor/Argyll/$PLATFORM_DIR"
|
||||
|
||||
FORCE=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--force) FORCE=1 ;;
|
||||
*) echo "usage: $0 [--force]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$FORCE" -eq 0 ] && [ -x "$DEST/$MARKER" ]; then
|
||||
echo "ArgyllCMS binaries already present at $DEST (use --force to re-download)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
AUTH_HEADER=""
|
||||
if [ -n "${GITEA_TOKEN:-}" ]; then
|
||||
AUTH_HEADER="Authorization: token $GITEA_TOKEN"
|
||||
fi
|
||||
|
||||
api_get() {
|
||||
if [ -n "$AUTH_HEADER" ]; then
|
||||
curl -fsSL -H 'Accept: application/json' -H "$AUTH_HEADER" "$1"
|
||||
else
|
||||
curl -fsSL -H 'Accept: application/json' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$TAG" ]; then
|
||||
API_URL="$SERVER/api/v1/repos/$REPO/releases/tags/$TAG"
|
||||
else
|
||||
API_URL="$SERVER/api/v1/repos/$REPO/releases/latest"
|
||||
fi
|
||||
|
||||
echo "Fetching release info from $API_URL"
|
||||
RELEASE_JSON="$(api_get "$API_URL")" || {
|
||||
echo "error: failed to fetch release info (set GITEA_TOKEN if the repo is private)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find the macOS universal asset's browser_download_url without jq.
|
||||
ASSET_URL="$(printf '%s' "$RELEASE_JSON" \
|
||||
| tr ',' '\n' \
|
||||
| grep '"browser_download_url"' \
|
||||
| grep "$SUFFIX" \
|
||||
| sed -E 's/.*"browser_download_url"[^"]*"([^"]+)".*/\1/' \
|
||||
| head -n 1)"
|
||||
|
||||
if [ -z "$ASSET_URL" ]; then
|
||||
echo "error: no release asset matching '*$SUFFIX' on $API_URL" >&2
|
||||
echo "looked-for pattern: Argyll_<tag>_<sha>$SUFFIX" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading $ASSET_URL"
|
||||
TMPDIR_FETCH="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_FETCH"' EXIT
|
||||
ARCHIVE="$TMPDIR_FETCH/argyll.tgz"
|
||||
|
||||
if [ -n "$AUTH_HEADER" ]; then
|
||||
curl -fSL -o "$ARCHIVE" -H "$AUTH_HEADER" "$ASSET_URL"
|
||||
else
|
||||
curl -fSL -o "$ARCHIVE" "$ASSET_URL"
|
||||
fi
|
||||
|
||||
EXTRACT="$TMPDIR_FETCH/extract"
|
||||
mkdir -p "$EXTRACT"
|
||||
tar -xzf "$ARCHIVE" -C "$EXTRACT"
|
||||
|
||||
# Archive contains Argyll_V*/bin/ (or a bare bin/).
|
||||
BIN_DIR=""
|
||||
for d in "$EXTRACT"/Argyll_V*/bin "$EXTRACT"/bin; do
|
||||
if [ -d "$d" ]; then BIN_DIR="$d"; break; fi
|
||||
done
|
||||
if [ -z "$BIN_DIR" ]; then
|
||||
echo "error: archive has no Argyll_V*/bin or bin/ directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST"
|
||||
cp -R "$BIN_DIR"/. "$DEST"/
|
||||
find "$DEST" -type f -exec chmod 0755 {} +
|
||||
# Downloads carry com.apple.quarantine; the app cannot spawn quarantined tools.
|
||||
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true
|
||||
|
||||
if [ ! -x "$DEST/$MARKER" ]; then
|
||||
echo "error: marker binary $MARKER missing after extraction" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: $(ls "$DEST" | wc -l | tr -d ' ') tools installed to $DEST"
|
||||
Reference in New Issue
Block a user