mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 00:49:14 -06:00
Merge pull request #7239 from haranishi/review/packaged-cli-docs-20261018
Linux packages: ship the CLI in source Flatpaks and offline automation guides
This commit is contained in:
@@ -5,9 +5,9 @@ name: Packaging lint
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**", "docs/mcp.md", "docs/control-protocol.md"]
|
||||
pull_request:
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"]
|
||||
paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**", "docs/mcp.md", "docs/control-protocol.md"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
lint:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/packaging-lint.yml@main
|
||||
with:
|
||||
extra-checks-script: packaging/linux/package-docs-test.sh
|
||||
|
||||
verified-appimage-inputs:
|
||||
timeout-minutes: 10
|
||||
|
||||
@@ -302,6 +302,48 @@ one is missing, prints the package to install and exits with status 1 instead of
|
||||
Locally (on Linux): install [nfpm](https://nfpm.goreleaser.com/install/), then
|
||||
`packaging/linux/package.sh` (or `--formats "deb tar"`).
|
||||
|
||||
#### Packaged automation and offline documentation
|
||||
|
||||
Both Flatpak manifests install `photocraft-cli`, alongside the GUI. To start the stdio MCP
|
||||
server with a project in the sandbox's allowed Documents directory:
|
||||
|
||||
```sh
|
||||
flatpak run --command=photocraft-cli ai.storyteller.photocraft mcp \
|
||||
--automation-read-root "$HOME/Documents/project" \
|
||||
--automation-write-root "$HOME/Documents/project"
|
||||
```
|
||||
|
||||
The automation roots restrict PhotoCraft's file access; they do not grant Flatpak access to
|
||||
other host directories. For a project elsewhere, also grant that directory with Flatpak's
|
||||
`--filesystem=/absolute/project` run option. Stdio MCP needs no network permission.
|
||||
|
||||
Linux packages include the original `mcp.md` and `control-protocol.md` for offline use:
|
||||
|
||||
| Format | Documentation directory |
|
||||
|---|---|
|
||||
| `.deb`, `.rpm` | `/usr/share/doc/photocraft/` |
|
||||
| Extracted `.tar.gz` | `share/doc/photocraft/` inside the extracted release directory |
|
||||
| Flatpak (source or bundle) | `/app/share/doc/photocraft/` inside the sandbox |
|
||||
| Extracted AppImage | `squashfs-root/usr/share/doc/photocraft/` |
|
||||
|
||||
For example, read the Flatpak's MCP guide without opening the GUI:
|
||||
|
||||
```sh
|
||||
flatpak run --command=cat ai.storyteller.photocraft /app/share/doc/photocraft/mcp.md
|
||||
```
|
||||
|
||||
To read an AppImage's guides, run `./PhotoCraft.AppImage --appimage-extract` in a directory
|
||||
where you want its `squashfs-root` contents, using the downloaded file's actual name. Then
|
||||
open either guide in `squashfs-root/usr/share/doc/photocraft/`. Extraction is an explicit user
|
||||
action; launching the app does not extract documentation into the home directory. These two
|
||||
Markdown files are included as written; links to other repository documents still need a
|
||||
checkout or the online documentation.
|
||||
|
||||
`bash packaging/linux/package-docs-test.sh` checks the real Linux staging/tar commands,
|
||||
AppDir retention and both Flatpak manifests' install commands with fixture binaries and
|
||||
stubbed AppImage tooling. Packaging lint runs it through `extra-checks-script`; it does not
|
||||
compile Rust, build a real AppImage, or run a Flatpak sandbox.
|
||||
|
||||
### FreeBSD
|
||||
|
||||
GitHub has no FreeBSD runners, so the `freebsd` job runs `packaging/freebsd/package.sh` in a
|
||||
|
||||
@@ -101,8 +101,11 @@ modules:
|
||||
CRAFT_I18N_DIR: /run/build/photocraft/craft-i18n
|
||||
CRAFT_I18N_REQUIRED: '1'
|
||||
build-commands:
|
||||
- cargo --offline build --release --locked -p photocraft --features heif
|
||||
- cargo --offline build --release --locked -p photocraft -p photocraft-cli --features heif
|
||||
- install -Dm755 target/release/photocraft /app/bin/photocraft
|
||||
- install -Dm755 target/release/photocraft-cli /app/bin/photocraft-cli
|
||||
- install -Dm644 docs/mcp.md /app/share/doc/photocraft/mcp.md
|
||||
- install -Dm644 docs/control-protocol.md /app/share/doc/photocraft/control-protocol.md
|
||||
- install -Dm644 NOTICE /app/share/doc/photocraft/NOTICE
|
||||
- install -Dm644 ATTRIBUTION.md /app/share/doc/photocraft/ATTRIBUTION.md
|
||||
- install -Dm644 packaging/linux/ai.storyteller.photocraft.desktop /app/share/applications/ai.storyteller.photocraft.desktop
|
||||
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exercise Linux staging and both Flatpak manifests without building Rust or an AppImage.
|
||||
# Needs the normal Linux packaging tools, Python 3 and PyYAML (packaging-lint installs them).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
python3 - "$ROOT" <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import yaml
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
docs = ("mcp.md", "control-protocol.md")
|
||||
|
||||
|
||||
def run(command, cwd, env):
|
||||
result = subprocess.run(command, cwd=cwd, env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode:
|
||||
raise AssertionError(f"{command!r} failed ({result.returncode}):\n{result.stdout}")
|
||||
|
||||
|
||||
def assert_docs(directory):
|
||||
for name in docs:
|
||||
path = directory / name
|
||||
assert path.is_file(), f"missing packaged document: {path}"
|
||||
assert path.read_bytes() == (root / "docs" / name).read_bytes(), f"changed packaged document: {path}"
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="photocraft-package-docs-") as temporary:
|
||||
temp = Path(temporary)
|
||||
fixture = temp / "source with spaces"
|
||||
fixture.mkdir()
|
||||
# Keep the real scripts, metadata, icons and document bytes. Only replace the binary
|
||||
# build and AppImage download/tool boundary; no real tool or runtime is downloaded.
|
||||
for name in ("packaging", "assets/app-icon/hicolor", "docs/mcp.md", "docs/control-protocol.md", "Cargo.toml", "README.md", "LICENSE", "LICENSE-MIT", "LICENSE-APACHE", "COPYRIGHT", "NOTICE", "ATTRIBUTION.md"):
|
||||
source, destination = root / name, fixture / name
|
||||
if not source.exists():
|
||||
continue
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_dir():
|
||||
shutil.copytree(source, destination)
|
||||
else:
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
binary = "#!/usr/bin/env bash\nprintf 'PhotoCraft packaging fixture\\n'\n"
|
||||
target = fixture / "target"
|
||||
for name in ("photocraft", "photocraft-cli"):
|
||||
path = target / "release" / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(binary)
|
||||
path.chmod(0o755)
|
||||
(fixture / "packaging/linux/verified-download.sh").write_text('''
|
||||
download_verified() {
|
||||
if [ "${4:-false}" = true ]; then
|
||||
cat >"$2" <<'TOOL'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Stand in for appimagetool's output, leaving the real AppDir for assertions below.
|
||||
touch "${@: -1}"
|
||||
TOOL
|
||||
chmod 755 "$2"
|
||||
else
|
||||
printf 'fixture runtime\\n' >"$2"
|
||||
fi
|
||||
}
|
||||
verify_appimagetool_override() { return 1; }
|
||||
''')
|
||||
env = os.environ.copy()
|
||||
for key in ("CROSS_TARGET", "CROSS_COMPILE", "EMULATOR", "APPIMAGETOOL", "CRAFT_FONTS_DIR"):
|
||||
env.pop(key, None)
|
||||
env.update(CARGO_TARGET_DIR=str(target), DIST=str(temp / "dist"), CROSS_ARCH="x86_64", PHOTOCRAFT_BUILD_DATE="2026-01-01")
|
||||
run(["bash", "packaging/linux/package.sh", "--skip-build", "--formats", "tar appimage"], fixture, env)
|
||||
assert_docs(target / "linux-package/root/usr/share/doc/photocraft")
|
||||
assert_docs(target / "linux-package/PhotoCraft.AppDir/usr/share/doc/photocraft")
|
||||
archives = list((temp / "dist").glob("*.tar.gz"))
|
||||
assert len(archives) == 1, f"expected one tarball, found {archives}"
|
||||
unpacked = temp / "unpacked"
|
||||
unpacked.mkdir()
|
||||
run(["tar", "-xzf", str(archives[0]), "-C", str(unpacked)], fixture, env)
|
||||
bundle = next(unpacked.iterdir())
|
||||
assert_docs(bundle / "share/doc/photocraft")
|
||||
assert (bundle / "bin/photocraft-cli").read_text() == binary
|
||||
print("PASS: FHS stage, tarball and AppDir retain both protocol documents")
|
||||
|
||||
# The source manifest must request both packages; a missing -p photocraft-cli leaves
|
||||
# no CLI for its real install command to copy. Do not seed its build with staged bins.
|
||||
shutil.rmtree(target)
|
||||
tools = temp / "tools"
|
||||
tools.mkdir()
|
||||
cargo = tools / "cargo"
|
||||
cargo.write_text('''#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys
|
||||
args = sys.argv[1:]
|
||||
for index, arg in enumerate(args[:-1]):
|
||||
if arg == "-p":
|
||||
path = Path("target/release") / args[index + 1]
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("#!/usr/bin/env bash\\nprintf 'PhotoCraft packaging fixture\\\\n'\\n")
|
||||
path.chmod(0o755)
|
||||
''')
|
||||
cargo.chmod(0o755)
|
||||
env["PATH"] = str(tools) + os.pathsep + env["PATH"]
|
||||
for suffix, directory in ((".yml", fixture), (".bundle.yml", bundle)):
|
||||
manifest = yaml.safe_load((root / f"packaging/linux/flatpak/ai.storyteller.photocraft{suffix}").read_text())
|
||||
app = temp / ("flatpak-source" if suffix == ".yml" else "flatpak-bundle")
|
||||
# Other modules, such as cups-client, build their own sources; only photocraft is checked here.
|
||||
module = next(m for m in manifest["modules"] if isinstance(m, dict) and m.get("name") == "photocraft")
|
||||
for command in module["build-commands"]:
|
||||
# Redirect only the sandbox output root, preserving the manifest's commands.
|
||||
command = command.replace("/app/", shlex.quote(str(app)) + "/")
|
||||
run(["bash", "-euo", "pipefail", "-c", command], directory, env)
|
||||
cli = app / "bin/photocraft-cli"
|
||||
assert cli.is_file() and os.access(cli, os.X_OK), f"missing executable CLI in {suffix}"
|
||||
assert cli.read_text() == binary, f"changed CLI bytes in {suffix}"
|
||||
assert_docs(app / "share/doc/photocraft")
|
||||
print(f"PASS: Flatpak {suffix} build commands install the CLI and both protocol documents")
|
||||
PY
|
||||
@@ -78,6 +78,8 @@ mkdir -p "$STAGE/usr/share/icons"
|
||||
cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/"
|
||||
mkdir -p "$STAGE/usr/share/doc/photocraft"
|
||||
copy_docs "$STAGE/usr/share/doc/photocraft"
|
||||
install -Dm644 "$ROOT/docs/mcp.md" "$STAGE/usr/share/doc/photocraft/mcp.md"
|
||||
install -Dm644 "$ROOT/docs/control-protocol.md" "$STAGE/usr/share/doc/photocraft/control-protocol.md"
|
||||
# photocraft-cli's man pages and shell completions, from its clap definition, written by the
|
||||
# example built above. A cross build runs it through $EMULATOR, like the --version check at the
|
||||
# end, and leaves them out without one, as a --skip-build run whose prebuilt binaries came without
|
||||
|
||||
Reference in New Issue
Block a user