Skip to content

Ship a Linux package with the release (1.5.0.0) - #8

Merged
dansleboby merged 16 commits into
mainfrom
feature/linux-package
Oct 5, 2026
Merged

dansleboby merged 16 commits into
mainfrom
feature/linux-package

Conversation

@dansleboby

@dansleboby dansleboby commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Linux package in every release: CanonWebAPI-linux-x64.tar.gz (folder CanonWebAPI/): self-contained single-file Canon.API for Linux x64 (no .NET or ICU to install) with libEDSDK.so, plus install.sh, which installs it as the canonwebapi systemd service in /opt/canonwebapi (hardened unit), with a udev rule that gives the service user the Canon PTP cameras. The rule uses final assignments: 60-libgphoto2*.rules sorts after it and would give the camera back to plugdev at every replug, power-on or reboot. install.sh checks the system and the package before changing anything and fails when the service does not stay up; install.sh --uninstall removes everything. Manual updates (run the install.sh of the new archive).
  • Package check: tools/Test-Package.ps1 starts a published package from a temporary copy and checks that /status answers (camera connected, or "No Canon camera detected"): it fails when the EDSDK library cannot be loaded.
  • CI on Windows and Linux: both runners build and test, then publish and check their package (the published Windows executable was never run before a release).
  • Atomic release pipeline: prepare (tag check, version) -> windows + linux in parallel (build, test, publish, check, archive) -> release with both packages and the AutoUpdater XML, only when both succeeded. The build jobs get a read-only token. workflow_dispatch runs a dry run that also downloads both packages and generates the XML in the release job, but creates no release and pushes nothing. The Windows package and the feed are unchanged (the generated XML is byte identical, UTF-8 BOM included).
  • Fatal startup error exits with code 1 (it exited 0): systemd Restart=on-failure and launchers see it.
  • Release 1.5.0.0 prepared: version, README upgrade note, changelog dated 2026-10-04. The tag is not pushed.

Design: specs/2026-10-04-linux-package-design.md, plan: specs/2026-10-04-linux-package-plan.md.

Test plan

  • Local (Ubuntu 26.04): the archive built as in the release job lists the expected files (owned by root, executable Canon.API and install.sh); Test-Package.ps1 passes on the package and fails without libEDSDK.so; install.sh simulated without root (install, update keeping appsettings.Local.json, re-run, double uninstall, missing package file); bash -n, shellcheck, udevadm verify, systemd-analyze verify clean; the release XML step produces the current docs/autoupdate.xml byte for byte.
  • CI: build-and-test (Windows) and build-and-test-linux pass, including both package checks.
  • install.sh on a real machine (Ubuntu 26.04) with the EOS R100:
    • service active and connected; camera device root:canonwebapi 0660 after the install and after two real power-ons (desktop user keeps its ACL);
    • tools/Test-Camera.ps1 against the service: 14/14 (live view, 4 captures in 1.2-1.3 s, 3 flash changes each followed by a capture, power cycle with the live view running, capture after it);
    • battery dead during the test: disconnection handled, service stayed up, reconnected when the camera came back;
    • update 1.4.0.0 -> 1.5.0.0: clean stop (camera session closed), logs kept, camera reconnected in 0.5 s;
    • uninstall removes the service, unit, rule, /opt/canonwebapi and the user; the camera returns to root:plugdev 0664.
  • After merge: dry run of the "Release CanonWebAPI" workflow on main, then the v1.5.0.0 tag on the owner's go-ahead.

🤖 Generated with Claude Code

ac-gpaquin and others added 16 commits October 4, 2026 19:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60-libgphoto2*.rules sorts after 60-canonwebapi.rules and, on add/bind
events of a PTP device, sets MODE="0664", GROUP="plugdev": after a reboot,
replug or camera power-on the service user could no longer open the
camera. install.sh only sent a change event, which libgphoto2 skips, so it
did not show right after the installation.

The rule now uses final assignments and only matches Canon PTP cameras
(printers and scanners are left alone). install.sh applies it with an add
event and waits for udev before starting the service; --uninstall sends
the same event so a plugged camera gets its default ownership back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install.sh only checked Canon.API up front: a missing libEDSDK.so, unit
or udev rule was found after the service had been stopped. It now checks
root, x86_64, that systemd runs as init (not only that systemctl exists),
libusb and every package file before the first change; --uninstall needs
root and systemd only.

After the restart it waits a few seconds and fails when the service is not
running, pointing to journalctl, instead of reporting a successful
installation of a service that crash-loops.

Also: the comment of the copy loop says what it leaves alone
(appsettings.Local.json and logs/), and the root check message no longer
ends with a space when there is no argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NoNewPrivileges, PrivateTmp, ProtectSystem=full (/opt, where the logs are
written, stays writable) and ProtectHome. PrivateDevices would hide the
camera, MemoryDenyWriteExecute breaks the JIT and an address family
restriction would block the netlink socket used for hot plug: the unit
says so, so they are not added later by mistake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The single file loaded libicu at runtime: on a minimal system the service
aborted with "Couldn't find a valid ICU package" and crash-looped. The API
only uses the invariant culture, so the net10.0 target now uses invariant
globalization. Checked with an ICU that cannot be found
(DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU=99.9): the previous build aborts,
this one answers /status.

The comment of the property group no longer reads as if it made the
publish self-contained: the publish command line does. README lists the
actual requirements (glibc 2.27, libstdc++ of GCC 7, libusb, systemd; no
ICU) and the tested distribution instead of untested examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catch around startup only logged the error, so the process exited
with code 0 when the port was in use or appsettings.Local.json was
invalid: launchers did not see the failure and systemd's
Restart=on-failure never fired. Checked on the published Linux package
with port 5097 held by another process: exit code 0 before, 1 now; a
normal stop (SIGTERM) still exits with 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The windows and linux jobs run NuGet build logic, the tests and the
package checks with a persisted contents: write token. The workflow now
defaults to contents: read, only prepare (version commit) and release
(GitHub release, AutoUpdater XML) get contents: write, and the build jobs
check out without persisting credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The release job also runs on workflow_dispatch: it checks out the
  selected branch, downloads both packages, lists them and generates the
  AutoUpdater XML (printed); only "Create GitHub Release" and "Commit
  AutoUpdater XML" are limited to tag pushes. A dry run now covers the
  download and the XML generation, and still publishes nothing.
- The AutoUpdater XML is written with a UTF-8 BOM, as Windows PowerShell
  always wrote it: the feed stays byte identical (pwsh on Linux dropped
  the BOM).
- The Linux archive records root (0/0) as owner: extracted with sudo, its
  files belonged to the runner's uid, whose local user could edit
  install.sh before root runs it.
- 30 minute timeout on the build jobs of both workflows (a hung check held
  a runner for 6 hours), and no comments that restate the code.

README states what a failure leaves behind: a failed build job publishes
nothing; a release job that fails after creating the release leaves the
feed (and the photo booths) on the previous version until it is rerun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: stop the service before running Canon.API by hand (one program
at a time can use the camera, and the service also listens on port 5000).
CLAUDE.md: the release no longer publishes only -f net10.0-windows; it
publishes the Windows package with -f net10.0-windows -r win-x64 and the
Linux package with -f net10.0 -r linux-x64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design still described the udev rule, the install.sh checks, the
service unit and the release dry run as they were before the fixes, and
the README requirements summary did not mention libstdc++.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dansleboby
dansleboby marked this pull request as ready for review October 5, 2026 03:22
@dansleboby
dansleboby merged commit 208442c into main Oct 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants