8 Commits
Author SHA1 Message Date
gronod 9102c95468 ci(macos): remove redundant cargo test step from release workflow
Build Windows Packages / Build Windows (push) Successful in 7m13s
Build Linux Packages / Build Linux (push) Successful in 10m27s
Build macOS Packages / Build macOS (Intel) (push) Failing after 10m35s
Merge pull request 'ci(macos): remove redundant cargo test step from release workflow' (#218) from development into main
2026-09-06 16:23:09 +01:00
gronod 743ad86f58 ci(macos): remove redundant cargo test step from release workflow 2026-09-06 16:22:57 +01:00
gronod 11e796fd17 Development
Build Windows Packages / Build Windows (push) Successful in 7m39s
Build Linux Packages / Build Linux (push) Successful in 9m54s
Build macOS Packages / Build macOS (Intel) (push) Failing after 14m45s
Merge pull request 'Development' (#217) from development into main
2026-09-06 16:08:49 +01:00
gronod ebbdbd4f55 chore: disable Apple Silicon and Universal build targets in macOS workflow 2026-09-06 16:06:24 +01:00
gronod ef0d65453e chore: configure git permissions in CI workflows and restrict crate source inclusion and build re-run triggers 2026-09-06 16:04:34 +01:00
gronod 9f2ff015d7 fix(cgats): support dataset file selection, sync wizard target state, and bump to v0.8.3 (#211) 2026-09-06 14:57:11 +01:00
gronod 5a1ad654b8 fix(colprof): add select_spectrum_file command for custom sp browse (#210) 2026-09-06 12:51:52 +01:00
gronod ec7eb2fcb4 test(gamut): fix gamut_viewer.test.js node execution and guard window globals (#212) 2026-09-06 12:48:43 +01:00
16 changed files with 204 additions and 55 deletions
+22 -25
View File
@@ -9,6 +9,8 @@ jobs:
build-macos: build-macos:
name: Build macOS (${{ matrix.platform.name }}) name: Build macOS (${{ matrix.platform.name }})
runs-on: ${{ matrix.platform.os }} runs-on: ${{ matrix.platform.os }}
env:
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
strategy: strategy:
matrix: matrix:
platform: platform:
@@ -17,21 +19,31 @@ jobs:
target: x86_64-apple-darwin target: x86_64-apple-darwin
binary_dir: macos-x86_64 binary_dir: macos-x86_64
arch: x86_64 arch: x86_64
- name: Apple Silicon # - name: Apple Silicon
os: macos # os: macos
target: aarch64-apple-darwin # target: aarch64-apple-darwin
binary_dir: macos-aarch64 # binary_dir: macos-aarch64
arch: arm64 # arch: arm64
- name: Universal (Intel + Apple Silicon) # - name: Universal (Intel + Apple Silicon)
os: macos # os: macos
target: universal-apple-darwin # target: universal-apple-darwin
binary_dir: macos-universal # binary_dir: macos-universal
arch: universal # arch: universal
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -59,21 +71,6 @@ jobs:
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist" test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen" test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
- name: Run Rust tests
shell: bash
env:
CARGO_INCREMENTAL: "0"
CARGO_TARGET_DIR: "${{ runner.temp }}/cargo-target"
run: |
cd src-tauri
if [ "${{ matrix.platform.target }}" = "universal-apple-darwin" ]; then
cargo test
elif [ "${{ matrix.platform.target }}" = "aarch64-apple-darwin" ] && [ "$(uname -m)" != "arm64" ]; then
cargo test --no-run --target ${{ matrix.platform.target }}
else
cargo test --target ${{ matrix.platform.target }}
fi
- name: Set Release Environment - name: Set Release Environment
id: set_env id: set_env
shell: bash shell: bash
+10
View File
@@ -35,6 +35,16 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
+4 -4
View File
@@ -163,9 +163,9 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries. - Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`. - Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
## CI & Cross-Compilation Testing ## CI & Cross-Compilation
- macOS CI workflow lives in `.gitea/workflows/build-macos.yml`. - Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`).
- On Intel runner hosts, cross-compiling for Apple Silicon (`aarch64-apple-darwin`) must use `cargo test --no-run --target aarch64-apple-darwin`. This validates sidecar packaging, compilation, and link correctness without attempting to execute ARM64 binaries on an Intel CPU (`Bad CPU type in executable (os error 86)`). - Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations.
- Universal and native Intel matrix jobs run and execute full tests natively. - Local/CI cross-compilation test execution for Apple Silicon (`aarch64-apple-darwin`) on Intel hosts must use `cargo test --no-run --target aarch64-apple-darwin` to avoid executing ARM64 binaries on an x86_64 CPU (`Bad CPU type in executable (os error 86)`).
+1 -1
View File
@@ -2,7 +2,7 @@
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS. > Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
[![Release](https://img.shields.io/badge/version-v0.8.2-blue.svg)](https://git.i3omb.com/gronod/ICCery) [![Release](https://img.shields.io/badge/version-v0.8.3-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery)
[![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app) [![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app)
[![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md) [![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md)
+6 -1
View File
@@ -14,7 +14,7 @@ ICCery is a native, cross-platform desktop application built with:
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout. - **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons. - **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`). - **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
- **Current Version**: `v0.8.2` (Production release). - **Current Version**: `v0.8.3` (Production release).
--- ---
@@ -108,6 +108,11 @@ ICCery is a native, cross-platform desktop application built with:
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel. - [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology). - [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
### Maintenance & Reliability Release (`v0.8.3`)
- [x] **Gamut Viewer Node Test Runner Support (#212)**: Guarded `window` and `window.__TAURI__` globals in `gamut_viewer.js` and added polyfill mock harness to `gamut_viewer.test.js` to enable automated headless test execution via `node src/js/gamut_viewer.test.js`.
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
--- ---
## 3. Future Roadmap ## 3. Future Roadmap
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "iccery", "name": "iccery",
"private": true, "private": true,
"version": "0.8.2", "version": "0.8.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs", "fetch-argyll": "node scripts/fetch-argyll.mjs",
+1 -1
View File
@@ -1423,7 +1423,7 @@ dependencies = [
[[package]] [[package]]
name = "iccery" name = "iccery"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image", "image",
+12 -1
View File
@@ -1,9 +1,20 @@
[package] [package]
name = "iccery" name = "iccery"
version = "0.8.2" version = "0.8.3"
description = "Modern Printer Profiling UI frontend for ArgyllCMS" description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"] authors = ["Gordon"]
edition = "2021" edition = "2021"
include = [
"src/**/*",
"build.rs",
"Cargo.toml",
"Cargo.lock",
"tauri.conf.json",
"capabilities/**/*",
"gen/**/*",
"icons/**/*",
"windows/**/*",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+6
View File
@@ -10,6 +10,12 @@ fn main() {
println!("cargo:rustc-env=BUILD_DATE={}", build_date); println!("cargo:rustc-env=BUILD_DATE={}", build_date);
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=argyll");
// Validate that ArgyllCMS sidecar binaries are staged before building // Validate that ArgyllCMS sidecar binaries are staged before building
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
+55
View File
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
rx.await.map_err(|e| format!("Dialog channel error: {}", e)) rx.await.map_err(|e| format!("Dialog channel error: {}", e))
} }
#[tauri::command]
pub async fn select_spectrum_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter("Argyll Spectrum File (*.sp)", &["sp"]);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_dataset_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter(
"Measurement Dataset (*.ti3, *.txt, *.cgats, *.csv)",
&["ti3", "txt", "cgats", "csv"],
);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command] #[tauri::command]
pub async fn select_target_file( pub async fn select_target_file(
app: AppHandle, app: AppHandle,
+2
View File
@@ -51,6 +51,8 @@ pub fn run() {
commands::inspect_dataset_preview, commands::inspect_dataset_preview,
commands::select_existing_target, commands::select_existing_target,
commands::select_profile_file, commands::select_profile_file,
commands::select_spectrum_file,
commands::select_dataset_file,
commands::select_target_file, commands::select_target_file,
commands::select_directory, commands::select_directory,
commands::send_stdin, commands::send_stdin,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery", "productName": "ICCery",
"version": "0.8.2", "version": "0.8.3",
"identifier": "com.gronod.iccery", "identifier": "com.gronod.iccery",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
+38 -9
View File
@@ -1,4 +1,6 @@
const { invoke } = window.__TAURI__.core; const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke
? window.__TAURI__.core.invoke
: async () => {};
export class CgatsInterop { export class CgatsInterop {
constructor(appState) { constructor(appState) {
@@ -22,26 +24,53 @@ export class CgatsInterop {
async handleImport() { async handleImport() {
try { try {
const filePath = await invoke('select_target_file'); const filePath = await invoke('select_dataset_file', {
defaultDir: this.appState?.cwd || null,
});
if (!filePath) return; // User cancelled if (!filePath) return; // User cancelled
// Inspect first to show modal (optional, skipping for now, directly import) const isWindows = filePath.includes('\\');
// Since we are mocking the UI a bit for this branch, we will just import directly const sep = isWindows ? '\\' : '/';
const parts = filePath.split(sep);
const fileName = parts.pop();
const fileDir = parts.join(sep);
const fileStem = fileName.replace(/\.[^/.]+$/, '');
const targetCwd = this.appState?.cwd || fileDir;
const targetBasename = this.appState?.basename || fileStem;
const targetBasenameInput = document.getElementById('targetBasename');
if (targetBasenameInput && !targetBasenameInput.value.trim()) {
targetBasenameInput.value = targetBasename;
}
const selectedPathDisplay = document.getElementById('selectedPathDisplay');
if (selectedPathDisplay && (!selectedPathDisplay.textContent || selectedPathDisplay.textContent.includes('No directory') || !this.appState?.cwd)) {
selectedPathDisplay.textContent = `Directory: ${targetCwd}`;
}
if (this.appState?.setTarget) {
await this.appState.setTarget(targetBasename, targetCwd);
}
const summary = await invoke('import_measurement_dataset', { const summary = await invoke('import_measurement_dataset', {
filePath, filePath,
targetCwd: this.appState.cwd, targetCwd,
targetBasename: this.appState.basename, targetBasename,
}); });
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success'); this.appState?.showNotice?.(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
// Update state to jump to stage 4 // Update state to jump to stage 4
if (this.appState?.updateGating) {
await this.appState.updateGating(); await this.appState.updateGating();
}
if (this.appState) {
this.appState.currentStage = 4; this.appState.currentStage = 4;
this.appState.applyStageDOM(4); this.appState.applyStageDOM?.(4);
}
} catch (e) { } catch (e) {
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error'); this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
} }
} }
+2 -2
View File
@@ -54,8 +54,8 @@ export function initColprof() {
if (btnBrowseCustomSp && colprofCustomSpPath) { if (btnBrowseCustomSp && colprofCustomSpPath) {
btnBrowseCustomSp.addEventListener("click", async () => { btnBrowseCustomSp.addEventListener("click", async () => {
try { try {
const selected = await window.__TAURI__.dialog.open({ const selected = await invoke("select_spectrum_file", {
filters: [{ name: 'Spectrum', extensions: ['sp'] }] defaultDir: chartreadCwd || wizardState.cwd || null,
}); });
if (selected) { if (selected) {
colprofCustomSpPath.value = selected; colprofCustomSpPath.value = selected;
+2 -3
View File
@@ -1,8 +1,7 @@
import { computeQuickHull } from "./vendor/quickhull.js"; import { computeQuickHull } from "./vendor/quickhull.js";
import { labToSrgb } from "./color_convert.js"; import { labToSrgb } from "./color_convert.js";
const { invoke } = window.__TAURI__.core; const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
const { listen } = window.__TAURI__.event;
let scene, camera, renderer, labelRenderer, controls; let scene, camera, renderer, labelRenderer, controls;
let currentProfileMesh = null; let currentProfileMesh = null;
+39 -4
View File
@@ -1,20 +1,50 @@
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing. // Unit & console tests for gamut_viewer.js parsing.
// Run in a browser/devtools console after the app has loaded: // Can be run in browser devtools console:
// import('./gamut_viewer.test.js').then(m => m.runAll()) // import('./gamut_viewer.test.js').then(m => m.runAll())
// Or in Node:
// node src/js/gamut_viewer.test.js
import { parseGamutFile } from './gamut_viewer.js'; // Node environment polyfill for browser globals
if (typeof window === 'undefined') {
globalThis.window = {
__TAURI__: {
core: { invoke: () => Promise.resolve() },
event: { listen: () => Promise.resolve(() => {}) }
},
addEventListener: () => {},
dispatchEvent: () => {}
};
globalThis.document = {
getElementById: () => null,
querySelectorAll: () => []
};
}
const { parseGamutFile } = await import('./gamut_viewer.js');
let passed = 0;
let total = 0;
export function runAll() { export function runAll() {
console.group('gamut/profcheck parser tests'); console.group('Gamut Viewer Parser Tests');
passed = 0;
total = 0;
testParseGamutBasic(); testParseGamutBasic();
testParseGamutDualTable(); testParseGamutDualTable();
testParseGamutWithComments(); testParseGamutWithComments();
console.log(`\nResults: ${passed} / ${total} tests passed.`);
console.groupEnd(); console.groupEnd();
if (passed !== total) {
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
}
} }
function assertEqual(actual, expected, message) { function assertEqual(actual, expected, message) {
total++;
const ok = JSON.stringify(actual) === JSON.stringify(expected); const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (ok) { if (ok) {
passed++;
console.log('PASS:', message); console.log('PASS:', message);
} else { } else {
console.error('FAIL:', message, 'expected', expected, 'got', actual); console.error('FAIL:', message, 'expected', expected, 'got', actual);
@@ -78,3 +108,8 @@ END_DATA`;
assertEqual(vertices.length, 4, 'commented gamut vertex count'); assertEqual(vertices.length, 4, 'commented gamut vertex count');
assertEqual(faces.length, 2, 'commented gamut face count'); assertEqual(faces.length, 2, 'commented gamut face count');
} }
// Auto-run if executed in Node.js
if (typeof process !== 'undefined' && process.argv && process.argv[1]?.endsWith('gamut_viewer.test.js')) {
runAll();
}