Author SHA1 Message Date
gronod a8c4025c01 Merge pull request 'Development' (#216) from development into main
Build Windows Packages / Build Windows (push) Successful in 7m38s
Build Linux Packages / Build Linux (push) Successful in 8m59s
Build macOS Packages / Build macOS (Apple Silicon) (push) Failing after 1m13s
Build macOS Packages / Build macOS (Universal (Intel + Apple Silicon)) (push) Failing after 1m28s
Build macOS Packages / Build macOS (Intel) (push) Failing after 51s
Reviewed-on: #216
2026-09-06 13:56:17 +01:00
18 changed files with 56 additions and 219 deletions
-3
View File
@@ -64,9 +64,6 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm ci run: npm ci
- name: Run frontend tests
run: npm test
- name: Set Release Environment - name: Set Release Environment
id: set_env id: set_env
shell: bash shell: bash
+25 -25
View File
@@ -9,8 +9,6 @@ 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:
@@ -19,31 +17,21 @@ 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,9 +47,6 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm ci run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries - name: Fetch ArgyllCMS binaries
env: env:
ARGYLL_SERVER_URL: ${{ github.server_url }} ARGYLL_SERVER_URL: ${{ github.server_url }}
@@ -74,6 +59,21 @@ 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
-3
View File
@@ -41,9 +41,6 @@ jobs:
- name: Install Node dependencies - name: Install Node dependencies
run: npm ci run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries - name: Fetch ArgyllCMS binaries
shell: powershell shell: powershell
env: env:
-10
View File
@@ -35,16 +35,6 @@ 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 ## CI & Cross-Compilation Testing
- Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`). - macOS CI workflow lives in `.gitea/workflows/build-macos.yml`.
- Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations. - 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)`).
- 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)`). - Universal and native Intel matrix jobs run and execute full tests natively.
+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.4-blue.svg)](https://git.i3omb.com/gronod/ICCery) [![Release](https://img.shields.io/badge/version-v0.8.2-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)
+1 -10
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.4` (Production release). - **Current Version**: `v0.8.2` (Production release).
--- ---
@@ -108,15 +108,6 @@ 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.
### Maintenance & Reliability Release (`v0.8.4`)
- [x] **Atomic Verification History Persistence (#213)**: Hardened `quality_store.rs` with atomic temporary file writes (`.tmp`), explicit flush/sync, and atomic rename to prevent historical drift data loss or corruption upon unexpected system crashes.
- [x] **Frontend Unit Testing & CI Integration (#215)**: Added standard `npm test` script executing the 3 frontend test suites (`profcheck`, `chartread`, and `gamut_viewer`) and integrated automated frontend test validation into macOS, Linux, and Windows CI workflows.
--- ---
## 3. Future Roadmap ## 3. Future Roadmap
+2 -3
View File
@@ -1,12 +1,11 @@
{ {
"name": "iccery", "name": "iccery",
"private": true, "private": true,
"version": "0.8.4", "version": "0.8.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs", "fetch-argyll": "node scripts/fetch-argyll.mjs",
"tauri": "tauri", "tauri": "tauri"
"test": "node src/js/profcheck.test.js && node src/js/chartread.test.js && node src/js/gamut_viewer.test.js"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2"
+1 -1
View File
@@ -1423,7 +1423,7 @@ dependencies = [
[[package]] [[package]]
name = "iccery" name = "iccery"
version = "0.8.4" version = "0.8.2"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image", "image",
+1 -12
View File
@@ -1,20 +1,9 @@
[package] [package]
name = "iccery" name = "iccery"
version = "0.8.4" version = "0.8.2"
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,12 +10,6 @@ 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,61 +391,6 @@ 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,8 +51,6 @@ 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.4", "version": "0.8.2",
"identifier": "com.gronod.iccery", "identifier": "com.gronod.iccery",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
+11 -40
View File
@@ -1,6 +1,4 @@
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke const { invoke } = window.__TAURI__.core;
? window.__TAURI__.core.invoke
: async () => {};
export class CgatsInterop { export class CgatsInterop {
constructor(appState) { constructor(appState) {
@@ -24,53 +22,26 @@ export class CgatsInterop {
async handleImport() { async handleImport() {
try { try {
const filePath = await invoke('select_dataset_file', { const filePath = await invoke('select_target_file');
defaultDir: this.appState?.cwd || null,
});
if (!filePath) return; // User cancelled if (!filePath) return; // User cancelled
const isWindows = filePath.includes('\\'); // Inspect first to show modal (optional, skipping for now, directly import)
const sep = isWindows ? '\\' : '/'; // Since we are mocking the UI a bit for this branch, we will just import directly
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, targetCwd: this.appState.cwd,
targetBasename, targetBasename: this.appState.basename,
}); });
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(); this.appState.currentStage = 4;
} this.appState.applyStageDOM(4);
if (this.appState) {
this.appState.currentStage = 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 invoke("select_spectrum_file", { const selected = await window.__TAURI__.dialog.open({
defaultDir: chartreadCwd || wizardState.cwd || null, filters: [{ name: 'Spectrum', extensions: ['sp'] }]
}); });
if (selected) { if (selected) {
colprofCustomSpPath.value = selected; colprofCustomSpPath.value = selected;
+3 -2
View File
@@ -1,7 +1,8 @@
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 = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null; const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
let scene, camera, renderer, labelRenderer, controls; let scene, camera, renderer, labelRenderer, controls;
let currentProfileMesh = null; let currentProfileMesh = null;
+4 -39
View File
@@ -1,50 +1,20 @@
// Unit & console tests for gamut_viewer.js parsing. // Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
// Can be run in browser devtools console: // Run in a browser/devtools console after the app has loaded:
// 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
// Node environment polyfill for browser globals import { parseGamutFile } from './gamut_viewer.js';
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 Viewer Parser Tests'); console.group('gamut/profcheck 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);
@@ -108,8 +78,3 @@ 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();
}