self_update::detect fell back to Install::Standalone for any executable it
could not place, so on macOS a Homebrew, MacPorts or Nix copy, or a program
inside another app's bundle, was replaced in place, and on Windows so was a
scoop, winget, Chocolatey or Store copy. The package manager's record then
stays on the old version, and the bundle belongs to another app.
macOS now treats executables under /opt/homebrew, /usr/local/Cellar,
/usr/local/Homebrew, /usr/local/opt, /opt/local and /nix as owned by a package
manager, and any executable inside an .app bundle that is not this app's own
as not ours; none of them self-update. Windows does the same for scoop, winget,
Chocolatey and WindowsApps folders. The rules are plain path functions, so
they are tested on every platform.
Tested: three new tests failed before the change (the macOS and Windows folder
rules, and detect on an executable in another app's bundle) and pass after it.
cargo test -p craft-update (209 tests), clippy -D warnings and fmt pass on
macOS; the Windows branch was not compiled here.
Fixes#390 for macOS and Windows; the Linux side is #388.
get_api shared the 4 MiB limit for small answers. A launcher version list of
100 releases can pass that: SolidCraft's was 6.5 MB (craft-launcher#1021).
Other small requests keep 4 MiB.
Brings in main's launcher version pins (craft-update: apps respect the
launcher's per-app version choice, and the follow-ups through 9817c81).
One conflict, in updater.rs's tests: main made the newer() helper
pub(super) for updater_pin_tests.rs, and this branch added its two #391
tests (download_opens_only_a_safe_release_page,
every_releases_page_is_a_safe_web_link) just above it. Kept both tests
and main's pub(super). Everything else merged cleanly: Download still
goes through download_action (safe_web_link, then the app's releases
page), "What's new" still checks safe_web_link, and main's pin checks
in offer, check_now, finish_check and start_install are untouched. Main
adds no new way for the prompt to open a URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Flatpak launcher wrote versions.json inside its own sandbox, and no Flatpak
app could read the launcher's folder, so pins never reached Flatpak apps, nor
host apps started from their menu entry. The file now sits in its own folder,
ArtCraft/versions under the host's per-user data folder, found from inside a
Flatpak too (HOST_XDG_DATA_HOME, else ~/.local/share). A Flatpak app reads it
with one read-only grant for that folder alone (pins::FLATPAK_GRANT).
folders::host_data_local_dir replaces install's private copy of the same rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ureq 3.5's Debug request log lists an allow-list of safe headers and
leaves Authorization out; the unredacted wire is its Trace output
(ureq-proto's log_data). `Http::with_github_token` now says so: apps
should redact Bearer tokens or keep `ureq` and `ureq_proto` at Debug or
quieter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Download closes the prompt, so a release link that fails
`release::safe_web_link` left the user with nothing opened. It now opens
`App::releases_url()` instead, which passes the check for every app in the
suite; a release link with `&` or another shell metacharacter is still
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Release`'s fields are public, so an app that builds one from its own
feed or cache could hand any URL to the prompt's "What's new" link and to
Action::OpenUrl after Download. Both now go through
`release::safe_web_link`, as the launcher does before every open_url.
`Http::with_github_token` says that ureq's debug logs print the Bearer
token, so apps should redact it or keep ureq below Debug.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
finish_check now re-reads the choice before looking at the answer, so an app
the launcher pinned mid-check reports UpToDate and an up-to-date managed()
whether the network found a release, nothing, or an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
finish_check used the choice cached when the check started, so a launcher
pin made during the network request still opened the stable prompt. It now
re-reads the choice when the check finishes, as offer does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for #449:
- A prompt opened while the app followed stable stayed open after the
launcher pinned it, and its Update button still installed the stable
release. Reading a managed choice now closes a prompt that is not
installing, and an install re-reads the choice before it starts. A
running or finished install keeps its prompt, which tick reports through.
- Pins::tracks listed an explicit Stable entry despite its docs.
- The pretend-version child test read the developer's real pins file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launcher lets someone keep an app on an older release or on nightlies,
but the app's own update check never knew, so a pinned app prompted to
update on every start and "Update and restart" undid the downgrade.
`pins` is one small file the launcher writes (versions.json in its per-user
data folder, or the absolute path in CRAFT_VERSIONS_FILE) and apps read:
app id to Stable, Nightly or Pinned(tag), the launcher's own serde shape.
A missing or damaged file means everyone follows stable; an entry a newer
launcher wrote is left to the launcher and kept on save.
`Updater` reads it before each check and before an offer. An app kept off
stable gets no prompt and no network call; a requested check reports
UpToDate, and `Updater::managed()` says why, so the public Status enum is
unchanged and no app breaks on a rev bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The update prompt drew the app name's first letter in a blue square and used that blue for
its buttons and highlights. App gains accent and developer, and the prompt now draws:
- App::icon_png (decoded once per context, PNG only, at most 1024 px a side and never past
the renderer's texture limit), keeping the letter badge for an app without a usable icon;
- App::accent filling its buttons, the badge and an UPDATE chip, with white or black text,
whichever contrasts more, so every Craft app's accent reaches WCAG AA (4.5:1) for these
small labels; the What's new link is body text with an accent underline (accent-coloured
text missed 4.5:1 for several apps). No accent keeps the default blue;
- "by <developer>" beside the app name: App::developer, ArtCraft (app::DEVELOPER) for every
app in the storytold organisation unless the app passes its own;
- its own button states: backgrounds for unfilled buttons (LATER, DISMISS) when hovered and
pressed, as the host app's style can hover in a colour its dark text doesn't read on, and a
2 px focus ring in the body text colour on every button (egui shows focus in that state)
and on the What's new link.
Apps opt in by passing their own App, e.g.
App { icon_png: Some(include_bytes!("…128x128…png")), accent: Some([0x3f, 0x5f, 0xe0]), ..suite::WORDCRAFT }.
* craft-print: log why the printer list is empty when lpstat can't run; document Flatpak needs
Flatpak apps saw "No printers found" with working CUPS printers: the Freedesktop
runtime has libcups but not lpstat/lp, and the sandbox had neither the CUPS socket
nor Avahi (driverless printers come from lpstat -e). Log the spawn failure and say
in the README what a Flatpak must bundle and grant.
* craft-print: log a failed lpstat -p -d; README separates what each Flatpak piece is for
Review (CodeRabbit): a non-zero lpstat exit was dropped silently, and the README
said any missing piece empties the printer list when only lpstat and the socket do.
* craft-print README: driver options need the PPD, remote servers need network, socket paths are remapped
Review (CodeRabbit): printer_options reads /etc/cups/ppd before lpoptions, so it is
the PPD, not lpoptions, that a Flatpak lacks; a client.conf server is reached only
with --share=network.
* craft-print README: scope the CUPS_SERVER override to one app; describe the stricter socket wrapper
Review (CodeRabbit): flatpak override without an app id applies to every app.
main split Czech's suggest_in into one helper per round for the
cognitive_complexity limit (storytold/wordcraft#4501). On this branch that code is the
shared affix_suggest::suggest_in (36 of 30), so cs/suggest.rs stays the
thin wrapper and the shared function gets the same split. The candidates
are offered in the same order; a dump of 13,081 calls over every
language's test dictionary matches the old engine except one: the typo
round counts the lower-cased letters, so "İ" (two letters lower case)
now gets "I" in Polish, where the old count stopped one letter short.
`path_as_rect` in the vendored hayro-interpret took any five-element path whose
points reached all four corners of its bounding box for that box. An open
polyline of one MoveTo and four LineTo passes that test when its ends sit on
the corners, so it was drawn as a closed rectangle and a clip of that shape was
dropped. VectorCraft #2920: an open zigzag in an .ai file opened as its
bounding box.
Port PdfCraft #3327 (hayro-interpret patch 35): a path is a rectangle only when
its four points are distinct, each step runs along one axis, and the last point
returns exactly to the first, closed with ClosePath or a fourth line.
Tests: `rectangles` in crates/pdf/tests/interpretation.rs (4 of its 5 tests
fail without the change) and `context::path_as_rect_tests` in the vendored
crate. SOURCE.json and the reader ledger record the change.
* craft-update: a Windows MSI copy updates itself in its own context, wherever it is installed
Self-update only recognised an MSI copy by the launcher's per-user folder (the HKCU InstallDir
key, or %LOCALAPPDATA%\Programs\<App>). Every app installed for everyone, in
Program Files\ArtCraft\<App> or the older Program Files\<App>, was treated as a loose folder and
updated by unpacking the portable zip over Program Files without permission.
- msi_context moves here from the launcher (craft-launcher crates/native/src/msi_context.rs at
b827fed, unchanged apart from visibility, MsiScope and scope_arguments). Windows Installer's
own registration decides a copy's context, for the launcher and the apps alike, so the
launcher can drop its copy and both routes follow one rule.
- New msi_copy module: a copy is an MSI copy when an App Paths entry names its exe (a registry
read, cheap enough for status checks). Its update asks Windows Installer for the context; the
App Paths hive is only the fallback when that lookup cannot run.
- The update stops, before the app exits, when the copy is registered in both hives, the lookup
is unclear, or Windows lists the app more than once.
- After the update the helper starts the exe the registrations name now, since the package may
have moved an exact old default folder.
- A failed self-update now names the Windows Installer log, as the launcher's installs do.
- The launcher's own per-user package is recognised and updated exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: a copy only takes the Windows Installer context of its own hive
Review follow-up (CodeRabbit on #419). The product code for the context lookup came from the
app's only uninstall record, which was not proven to own the running exe. A package writes App
Paths under HKMU, always the hive of its own context, so a registration whose Windows Installer
context differs from the hive that named this exe belongs to another copy. The update now stops
then, instead of taking the other copy's context.
Also documents the remaining helpers the diff touches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: an app in the old per-user folder is judged by its registration first
Review follow-up (CodeRabbit on #419). The folder rule for the launcher's per-user package also
matched an app installed in %LOCALAPPDATA%\Programs\<App>, and it ran before the App Paths
check, so such a copy skipped the product-count, conflict and context checks. Now an app is
judged by its registration first; the folder rule only covers an app copy no registration names.
The launcher in its own folder still keeps its old path whatever is registered: on a real PC it
has an App Paths entry and a stale second uninstall record, and registration-first would have
stopped its self-update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-update: when the Windows Installer lookup cannot run, the uninstall records decide
Review follow-up (CodeRabbit on #419). The fallback trusted the App Paths hive, so a stale entry
or a product registered in both scopes could run the update in the wrong context. The fallback
now follows the launcher's rule: the context every uninstall record of the product agrees on
(msi_context::single_scope over HKLM in both views and HKCU). Missing or mixed records stop the
update, and an agreed context that differs from the App Paths hive still stops it as another
copy's registration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #423). If LOCALAPPDATA\Programs or Programs\ArtCraft is itself a
junction, the exe's folder resolves through it but a default path built from a resolved
LOCALAPPDATA does not, so a default install was planned as a chosen folder. The Windows layer now
resolves each complete default folder (new default_folders) and hands them to the planner, which
only compares.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit and CI on #423):
- The profile roots and LOCALAPPDATA are canonicalised like the exe's folder, so a profile behind
a junction or a subst drive still matches its default folder and still counts as the profile.
- When a failed move leaves no program to start, the helper starts nothing, so the recorded
"run its installer" message is not replaced by a restart error.
- The default-folder test builds its expected path the way the plan does, so it also passes
where Path::join uses '/' (Linux CI).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported by FredApps: PhotoCraft built from main as 0.17.0-integration.20261029.f5b3f123 offered
"Update and Restart" to 0.17.0, a downgrade. Semver puts a pre-release before its release, and
the update check and the self-update guard compared with plain semver.
New version::offers_update: a pre-release label the suite publishes (alpha, beta, rc, nightly)
sorts as before; any other label marks a build from source, which is only offered a strictly
higher X.Y.Z. check_for_update (through a new testable offered_release) and the self-update
guard both use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporary (craft-libs#422, step 4): new msi_migrate module, to be removed two releases after the
app packages become per-machine only.
The released packages refuse a per-machine install while a per-user copy exists, so the move
removes first and installs second: remove the per-user product (no permission prompt; if that
fails nothing has changed), install the new version for everyone, and if that fails or
permission is refused, install the new version for the account again in its old folder.
Settings and documents live outside the program folder and are never touched.
Which copies move: a per-user default folder goes to Program Files\ArtCraft\<App>; a chosen
folder outside the profile moves in place; a chosen folder inside the profile keeps updating
for the account. The launcher's own package never moves.
Tested with real Windows Installer runs of a renamed dual-scope package carrying the apps' own
install-policy.wxi: the move with the permission prompt accepted (twice), and with it refused
(the app came back for the account at the new version, the failed log kept). Also unit tests for
every plan and the order of steps, and a mutation check (9 of 9 killed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The fallback trusted the App Paths hive, so a stale entry
or a product registered in both scopes could run the update in the wrong context. The fallback
now follows the launcher's rule: the context every uninstall record of the product agrees on
(msi_context::single_scope over HKLM in both views and HKCU). Missing or mixed records stop the
update, and an agreed context that differs from the App Paths hive still stops it as another
copy's registration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The folder rule for the launcher's per-user package also
matched an app installed in %LOCALAPPDATA%\Programs\<App>, and it ran before the App Paths
check, so such a copy skipped the product-count, conflict and context checks. Now an app is
judged by its registration first; the folder rule only covers an app copy no registration names.
The launcher in its own folder still keeps its old path whatever is registered: on a real PC it
has an App Paths entry and a stale second uninstall record, and registration-first would have
stopped its self-update.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #419). The product code for the context lookup came from the
app's only uninstall record, which was not proven to own the running exe. A package writes App
Paths under HKMU, always the hive of its own context, so a registration whose Windows Installer
context differs from the hive that named this exe belongs to another copy. The update now stops
then, instead of taking the other copy's context.
Also documents the remaining helpers the diff touches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Self-update only recognised an MSI copy by the launcher's per-user folder (the HKCU InstallDir
key, or %LOCALAPPDATA%\Programs\<App>). Every app installed for everyone, in
Program Files\ArtCraft\<App> or the older Program Files\<App>, was treated as a loose folder and
updated by unpacking the portable zip over Program Files without permission.
- msi_context moves here from the launcher (craft-launcher crates/native/src/msi_context.rs at
b827fed, unchanged apart from visibility, MsiScope and scope_arguments). Windows Installer's
own registration decides a copy's context, for the launcher and the apps alike, so the
launcher can drop its copy and both routes follow one rule.
- New msi_copy module: a copy is an MSI copy when an App Paths entry names its exe (a registry
read, cheap enough for status checks). Its update asks Windows Installer for the context; the
App Paths hive is only the fallback when that lookup cannot run.
- The update stops, before the app exits, when the copy is registered in both hives, the lookup
is unclear, or Windows lists the app more than once.
- After the update the helper starts the exe the registrations name now, since the package may
have moved an exact old default folder.
- A failed self-update now names the Windows Installer log, as the launcher's installs do.
- The launcher's own per-user package is recognised and updated exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 02 §4: the decoder refuses the G5 with Error::Unsupported; the embedded-JPEG fallback is the
caller's, not the decoder's.
- 04: the Auto-slot claim holds outside the white-balance preset captures.
- The record README no longer says it holds everything in the order it happened: the derivation
trail stays at the pinned LightCraft revision.
- docs/clean-room.md allows that split for a derivation done in another repository, so the
standard and the CRW record agree.
main's workspace lints now deny cognitive_complexity above 30, and decode_entropy scored 41.
The emit macro becomes a Chains struct, unstuffing its own function, and the three stream-end
checks one. Same algorithm: the pinned CC0 digests still match byte for byte.
Also correct two record lines: the G5 is the one file that does not match, and the frozen
code tables are not the only logs that ship.
* Add craft-dialogs: native file and message dialogs that never block the event loop
Requests queue and show at the end of the frame on the main thread; answers come
back with the app's continuation on a later frame. One dialog at a time, a drop
guard and a panic guard so a dialog never stays open, and an override for tests.
On macOS, Open and Folder show as a free-standing NSOpenPanel
(beginWithCompletionHandler, which rfd can't show), Save and message boxes as
sheets through rfd's async dialogs, never falling back to runModal. Filter
extensions match in any letter case on Linux.
* craft-dialogs: Dialogs::raise brings the open macOS panel to the front
* CI: dialogs workflow (Linux clippy and tests for craft-dialogs)
* craft-dialogs: per-instance macOS panel, parent window check, override panic guard, CI lints macOS code
* craft-dialogs: raise only a visible panel; Open and Folder need a running app
A system install was offered only as a download. Reinstalling that bundle
through Flatpak's install-bundle action is denied without a password dialog
when polkit cannot fill in $(path). Use pkexec on the host instead, and keep
per-user bundles on flatpak install --user.
Co-authored-by: Cursor <cursoragent@cursor.com>