For developers. For an overview, start with the README. The app itself: desktop-app.md.
| Path | What it is |
|---|---|
crates/sync |
Rust crate lightcraft-cloud-client: the sync client shared by the apps (talks to the remote through librclone, reads the manifest, fetches thumbnails and previews, sends edits back; the phone app uses it) and, in its home module, the PC side: publishing a photo folder, applying the phone's edits, merging sidecars. |
apps/desktop |
lightcraft-cloud-desktop: the PC app: an always-running tray app (and the command line). Two builds: the full one is the home PC (renders thumbnails and smart previews with LightCraft's engine) or another PC; the light one (--no-default-features, no engine) is another PC only. See "Desktop app" and "Another PC" in desktop-app.md. |
tools/parity |
The harness that runs the Python tool and the Rust app on the same generated photos and compares everything they write. |
legacy/ |
The first PC tool, lc-cloud, written in Python. lightcraft-cloud-desktop replaces it; see legacy-lc-cloud.md. |
Rust 1.99.0 (rust-toolchain.toml picks it). librclone is loaded at run time, so it is not needed to build.
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warningsThe desktop app has two builds. The default one (feature home) is the full app and needs LightCraft's engine to render
previews. It builds and runs on Linux and on Windows (x86-64, MSVC). cargo build --release -p lightcraft-cloud-desktop --no-default-features is the light build for another PC: the same
tray app, windows and sync client, with no LightCraft crate besides the shared sync crate in cargo tree, and a much smaller
binary. Given a config for the home PC it says so and does nothing. Run the tests and clippy for both
(cargo test -p lightcraft-cloud-desktop --no-default-features).
The tests that talk to a real rclone need a host build of librclone.so. Point LIBRCLONE at it, for example
LIBRCLONE=~/src/lightcraft-android/apps/lightcraft-android/librclone-host/librclone.so cargo test --workspace
(built by apps/lightcraft-android/scripts/build-librclone.sh host in the phone app repository). Without it those tests
print "skipped" and pass.
The sync crate's tests drive the lightcraft-android engine (tag v0.1.0, pinned by rev in crates/sync/Cargo.toml), because the
upstream LightCraft engine does not have the command library.addRemote yet. The desktop app keeps the upstream engine
(v0.17.0), so Cargo.lock holds both. Bump the rev when the phone fork's engine changes. Changing the app's engine version
re-renders nothing by itself (see "Rendering" in desktop-app.md).
Tests that are not portable. cargo test --workspace is run on Windows and on Linux by CI (desktop.yml, job "full build and all tests"), with no
test skipped by default: a test that cannot run prints skipped: or SKIPPED: and the job fails on the word. Tests that make symbolic links need the
privilege to (a CI runner has it; LC_REQUIRE_SYMLINKS=1 turns a missing privilege into a failure instead of a printed skip). What is limited to one system, and why:
| Test | Runs on | Why |
|---|---|---|
crates/sync/tests/home_windows.rs (a sidecar or photo another program holds) |
Windows | Linux has no mandatory file locks, so nothing there can be held. |
other_import.rs: a_destination_the_home_pc_would_reject... (CON, a:b, ends. as folder names) |
Linux | Windows cannot create such folders, so no photo can be in one; the rule itself is tested on every system by safe_rel_on and the home import tests. |
src/pending.rs mod tests (written with POSIX paths) |
Linux | They are not absolute paths on Windows; the module windows_tests beside it covers the same rules with Windows paths. |
src/autostart.rs |
Linux: the .desktop file; Windows: the Run key (in a registry key of the test's own) |
The two systems store "start when I log in" differently. |
src/app.rs fake_exe |
Linux: a shell script; Windows: the fake_window example |
A script cannot be started by CreateProcess as a program. |
config.rs a_tilde_means... |
both, with different results for ~\ |
A backslash is an ordinary character in a Linux file name. |
util.rs Windows tests (long paths, a held file) |
Windows | The Win32 calls they test do not exist elsewhere. |
The slow engine tests (apps/desktop/tests/engine.rs, home_e2e.rs) render with the CPU on both systems (LIGHTCRAFT_GPU=0 in CI).
.github/workflows/release.yml builds both builds on windows-latest (--no-default-features: the light build, no engine, role "another PC"; and the default features: the home
build with the engine) and packs each into an installer with Inno Setup (one script, packaging/windows/lightcraft-cloud.iss; /DHome makes the home one):
lightcraft-cloud-<version>-windows-setup.exe (light; this name must never change, every v0.2.0 app's updater looks for it) and lightcraft-cloud-<version>-windows-home-setup.exe
(the windows-latest image ships Inno Setup 6.7.1, which CI uses and prints;
if the image ever lacks it, CI installs 6.6.1 with Chocolatey; the script needs 6.3 or newer). Each installer is per user (no administrator): %LOCALAPPDATA%\Programs\LightCraft Cloud\ with the
exe, librclone.dll (rclone's librclone built from tools/librclone, rclone v1.75.2, with Go and cgo), the licences and
README.txt (packaging/windows/README.txt for the light one, README-home.txt for the home one). desktop.yml builds, lints and tests both builds on every push (and the whole workspace's tests on Windows
and Linux), but makes no zip.
-
One identity, two editions. Both installers have the same
AppId, folder, Start menu entry, Run value and uninstall entry; the installer writesedition.txt(homeorlight) next to the exe, and--versionsays the same on its second line. Installing the home installer over the light app is an upgrade. The light installer over the home app is refused byInitializeSetup(nothing is changed, the exit code is 1) because it would replace the engine and silently stop the home PC; an install from v0.2.0 has noedition.txtand counts as light. After an uninstall (which removesedition.txt) either goes on.test-installer.ps1 -Variant light|home -OtherSetup <the other installer>tests all of that on the runner. The same-AppId choice means one program per PC (the instance lock would stop a second copy anyway) and a plain upgrade path from "other PC" to "home PC". -
The C runtime is inside the exe.
.cargo/config.tomlsets-C target-feature=+crt-staticforx86_64-pc-windows-msvc, socargo build --release -p lightcraft-cloud-desktop(either build) needs no Visual C++ Redistributable on a clean PC. CI proves it per build:tools/ci/pe_imports.pylists the exe's imports (and delay-loaded ones) and fails on anyvcruntime*,msvcp*,ucrtbaseorapi-ms-win-crt-*import, and on any DLL that is not a file ofSystem32(API sets excepted). -
The installer stops a running app with the new exe's
quitcommand, replaces the files, and starts the app again; if the install fails it starts the old one.lightcraft-cloud-desktop quit [--wait SECONDS]writes<state>/quit-request; the running app notices it within two seconds, deletes it and ends after the job it is running. Exit codes: 0 stopped, 10 not running, 11 still running after the wait (default 60 s), 12 not answered (an app from beforequitexisted). The installer ends what is left withtaskkillafter 11 and 12. A sync survives that: files are written to a temporary name and renamed, the manifest is written last, the run lock is an OS file lock that goes with the process, and the next sync carries on. Leftover.part-*files are removed after a day. -
"Start when I log in" is a task, on by default. It writes the same value as the Settings box,
HKCU\Software\Microsoft\Windows\CurrentVersion\RunLightCraft Cloud="<exe>" run. A silent run (the updater's) keeps what the user has: a value that is there is rewritten to the new location, one that is not stays off. -
Uninstall removes the program files and that value. It keeps
%APPDATA%\lightcraft-cloud-desktop,%LOCALAPPDATA%\lightcraft-cloud-desktopand%APPDATA%\rclone, and never looks at a photo folder. -
packaging/windows/test-installer.ps1is the test CI runs on the real Windows runner (silent install to the default place,--version, the Run value, the Start menu and Apps & features entries, install again over a running copy,quit, a Run value that was turned off or points at the zip, silent uninstall, planted files that must survive). A runner has no tray session, so it cannot show the tray icon, a click, the wizard's pages or SmartScreen. -
Config
%APPDATA%\lightcraft-cloud-desktop\config.toml, state and log%LOCALAPPDATA%\lightcraft-cloud-desktop\, rclone's config%APPDATA%\rclone\rclone.conf(orRCLONE_CONFIG/rclone_config). -
The exe is a windows-subsystem program: no console opens for the tray app. In a terminal the commands (
status,sync --once,run --headless,quit) print as usual, though the prompt may come back before they finish. -
Telling LightCraft about downloaded photos (only needed for photos that arrive from the home PC) uses LightCraft's control port, which is off by default and which Windows users cannot enable the way the Flatpak does. Either start LightCraft with
lightcraft.exe --control 7980(edit the shortcut's Target; installed in%LOCALAPPDATA%\Programs\ArtCraft\LightCraft, orC:\Program Files\ArtCraft\LightCraftfor all users), or setsetx LIGHTCRAFT_CONTROL_PORT 7980once and restart LightCraft (control protocol: loopback only, no authentication). The app'slightcraft_control_port(default 7980, 0 = off) must match. LightCraft's Windows installer also installslightcraft-cli.exe, but this app does not need it: it speaks the control protocol over a TCP socket itself. Without the port the photos still land in the photo folder; run Synchronize Folder in LightCraft. The same port is how new phone edits show up in LightCraft by themselves (see "Showing new edits in LightCraft" in desktop-app.md); it also lets the app tell you when LightCraft is not saving edits to sidecars, which on Windows is the usual reason edits never reach the phone.
A release is made from a commit on main whose desktop.yml run is green. Nothing publishes by itself.
- Set the version in
Cargo.toml([workspace.package] version; the desktop crate and the sync crate follow it), runcargo checksoCargo.lockfollows, update the docs, merge. - Tag it and push the tag:
git tag v0.2.0 && git push origin v0.2.0. The tag must bevplus the Cargo version; the workflow stops otherwise. release.ymlbuilds, on real runners: the two Windows installers (and runstest-installer.ps1against each, with the other as-OtherSetup),linux-lightandlinux-homeon Ubuntu 22.04 (the oldest runner, so glibc 2.35 is the minimum; the job prints the real minimum and fails if it is higher; each archive'sinstall.shis run in a throwaway home folder), andSHA256SUMSof the four files. Then it creates a draft release with those five files and generated notes. A tag with a-in it (v0.3.0-rc.1) makes a draft prerelease.- Open the draft on GitHub, read the notes, try the installer on a real Windows desktop (tray icon, Status window, SmartScreen), and publish it by hand.
The in-app updater only sees published releases (never drafts or prereleases), and it needs, for the light app,
lightcraft-cloud-<version>-windows-setup.exeand, for the home app,lightcraft-cloud-<version>-windows-home-setup.exe, withSHA256SUMS, in the release; an app offers nothing when the file of its own kind is missing.
workflow_dispatch runs the same builds and checks and publishes nothing. The updater reads
https://api.github.com/repos/tinytom21/lightcraft-cloud/releases/latest; its address, and the address installers are downloaded from, are constants in apps/desktop/src/update.rs
(there is deliberately no setting, environment variable or option for them; the tests use a local server through a function only tests can call).
tools/parity/parity.py # generated photos under /tmp/lc-parity; needs rclone, the LightCraft Flatpak and cargoIt generates a photo folder (procedural scenes from LightCraft's lightcraft-scenes, a synthetic DNG with a JPEG of the same
stem, sidecars, an identical copy, an excluded folder; no real photos), copies it twice, and runs lc-cloud sync on one copy
and lightcraft-cloud-desktop sync --once on the other, each with a throwaway config and state folder and a local-folder
remote (the Python tool is given --config, so it never reads ~/.config/lightcraft-cloud/; RCLONE_CONFIG is an empty
file). Five rounds: first publish; new, changed and removed photos and edited sidecars; fourteen synthetic phone edits in the
inbox (apply, merge, newest wins both ways, a chain of edits, an identical edit, edits for the raw and the JPEG of a pair, a
crs attribute-style sidecar, and three kinds of reject); publishing what the inbox wrote; a quiet run. After each it compares
the remote file lists, the manifests field by field, thumbnails (JPEG, dimensions) and smart previews (header), the state rows,
the version stores and, after the inbox, the sidecars byte for byte, the local history and the inbox lines of the log. A last
check lets the Rust app take over the Python tool's remote with a fresh state folder (nothing is uploaded again). Because the
Flatpak is the same engine release, thumbnails and smart previews come out byte-identical as well; the harness only requires
the same format and dimensions.
Result of the last run: no unexpected difference. Intended ones: publisher in the manifest (lightcraft-cloud-desktop <version>
instead of lightcraft-cloud 0.1.0) and generated.
Tests: cargo test --workspace runs about a hundred tests of the home side against a folder standing in for the remote with a fake
renderer (ports of the Python tool's scenarios: scan rules, exclude, pairing, manifest, inbox, merge, versions, dry run, safety,
publishing and history), plus tests with the real engine in apps/desktop/tests and, when LIBRCLONE is set, through librclone.