mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
Merge commit 'c48102350b1ef00614b105ac07451ff0ca8a5141' into fix/boolean-stroke-outlines
This commit is contained in:
+7
-3
@@ -103,11 +103,15 @@ todo = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
||||
# Workspace crates keep file:line in panic backtraces; dependencies carry no debug info. Both make
|
||||
# the test build faster and the target dir smaller.
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
debug = false
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! The binary end to end.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Depth refusal through the actual CLI, including its exit status and destination writes.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_doc::color::{Color, Paint};
|
||||
use figcraft_doc::{Document, NodeKind};
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Headless runs lay text out with the installed fonts, like the desktop app, unless
|
||||
//! `--no-system-fonts` or `FIGCRAFT_NO_SYSTEM_FONTS=1` opts out.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod cli;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod export_depth;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod fonts;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod pdf;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod pdf_print;
|
||||
@@ -1,5 +1,4 @@
|
||||
//! `figcraft-cli export --format pdf` and `convert … .pdf` end to end.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! `figcraft-cli export` with PDF print options: bleed, marks, CMYK; bad lengths are usage errors.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
@@ -261,7 +261,9 @@ fn fuzzed_frames_dont_hurt_the_server() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = ws.read();
|
||||
// read() returns Result<Message>, not a lock guard; the fuzz loop ends here on purpose and
|
||||
// the close frame from the server is not interesting.
|
||||
let _ = ws.read(); // NOSONAR
|
||||
}
|
||||
// Still serving, the file intact.
|
||||
let mut b = join(&p.edit, "Bo");
|
||||
|
||||
-1
@@ -2,7 +2,6 @@
|
||||
//! holds confirmed edits (#408): the log is replayed over the snapshot, which only the changes the
|
||||
//! snapshot lacks survive, and the damage is reported. A genuine leftover of a compaction cut short
|
||||
//! still restores silently.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_collab::message::{Access, ClientMsg};
|
||||
use figcraft_collab::{Client, Outgoing, Replica, Server};
|
||||
@@ -0,0 +1,5 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod header_recovery;
|
||||
mod problem_retention;
|
||||
@@ -141,7 +141,9 @@ fn old_server() -> (Endpoint, std::thread::JoinHandle<()>) {
|
||||
let t = std::thread::spawn(move || {
|
||||
for stream in listener.incoming().take(2).flatten() {
|
||||
let Ok(mut ws) = tungstenite::accept(stream) else { continue };
|
||||
let _ = ws.read();
|
||||
// read() returns Result<Message>, not a lock guard; this test server deliberately
|
||||
// ignores whatever the client sent first.
|
||||
let _ = ws.read(); // NOSONAR
|
||||
let refused = ServerMsg::Refused { reason: "that message couldn't be read".into() };
|
||||
let _ = ws.send(Message::Binary(Bytes::from(message::encode(&refused).unwrap())));
|
||||
let _ = ws.close(None);
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_engine::{
|
||||
Session,
|
||||
doc::NodeKind,
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//! Accepting a library update that changes only a paint style refreshes the layers linked to it.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::disallowed_methods)]
|
||||
|
||||
use figcraft_doc::{NodeId, StyleKind};
|
||||
use figcraft_engine::Session;
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//! `text.listIndent` / `text.listOutdent` report `changed` only when the document changed.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_engine::{
|
||||
Session,
|
||||
@@ -0,0 +1,14 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod empty_text_box_undo;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::disallowed_methods)]
|
||||
mod library_style_acceptance;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod list_indent_response;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod scope_reset;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod selection_colors;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod shape_builder_response;
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use figcraft_color::Paint;
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::{
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
mod distributed_alignment;
|
||||
mod import_link_contract;
|
||||
mod range_hyperlink_round_trip;
|
||||
@@ -1,8 +1,7 @@
|
||||
//! End-to-end agent tasks: an in-process MCP client completes realistic design tasks only through
|
||||
//! MCP tool calls, and checks every result through inspect_document / get_node like an agent would.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod common;
|
||||
use crate::common;
|
||||
|
||||
use common::{Client, approx, png_dims};
|
||||
use serde_json::{Value, json};
|
||||
@@ -1,6 +1,5 @@
|
||||
//! The attached backend against a fake app: a loopback control server answering with a headless
|
||||
//! session, speaking the desktop app's JSON-lines protocol.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::TcpListener;
|
||||
@@ -1,6 +1,5 @@
|
||||
//! `--automation-read-root` / `--automation-write-root` over MCP (#715): a tool call reads only
|
||||
//! inside the read and write roots and writes only inside the write roots.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::disallowed_methods)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! An in-process MCP client: feeds JSON-RPC lines to `Server::handle_line`.
|
||||
#![allow(dead_code, clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_mcp::Server;
|
||||
use serde_json::{Value, json};
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
//! Hidden top-level layers in an export take no room in the image (issue #447).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod common;
|
||||
use crate::common;
|
||||
|
||||
use common::{Client, png_dims};
|
||||
use serde_json::{Value, json};
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Fuzz-ish: no input line may crash the server or leave it unable to answer.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use figcraft_mcp::{Server, TOOL_NAMES};
|
||||
use proptest::prelude::*;
|
||||
@@ -0,0 +1,18 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod agent_tasks;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod attached;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::disallowed_methods)]
|
||||
mod automation_roots;
|
||||
#[allow(dead_code, clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod common;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod export_hidden_roots;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod garbage;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod protocol;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod screenshot_pixel_limit;
|
||||
@@ -1,7 +1,6 @@
|
||||
//! JSON-RPC / MCP protocol behaviour.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
mod common;
|
||||
use crate::common;
|
||||
|
||||
use common::Client;
|
||||
use figcraft_mcp::{PROTOCOL_VERSION, Server, TOOL_NAMES};
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
//! `screenshot` with `ids` must honour `maxSize` for the rendered paint area (shadows, overflow).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod common;
|
||||
use crate::common;
|
||||
use common::{Client, png_dims};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
//! A canvas pan (middle button or Space + primary) ends when its button is released, even when the
|
||||
//! release happens over a panel: returning to the canvas with no button held must not move the view.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use egui::{Event, Key, PointerButton, Pos2, RawInput, Rect, vec2};
|
||||
use figcraft_engine::Session;
|
||||
@@ -1,6 +1,5 @@
|
||||
//! File > Export frames to PDF: the menu asks where to save and writes the frames' PDF there,
|
||||
//! one visible top-level frame per page.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use egui::{Pos2, RawInput, Rect, Vec2, vec2};
|
||||
use figcraft_engine::Session;
|
||||
use figcraft_ui_egui::{FigApp, NoServices};
|
||||
@@ -0,0 +1,11 @@
|
||||
//! The crate's integration tests, built as one test binary.
|
||||
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod canvas_pan_release;
|
||||
mod comment_open_other_page;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod file_menu_pdf;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod guide_label_narrow;
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
mod recent_file_names;
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//! Recent file rows keep their full accessible name.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use egui::{Context, Id, Pos2, RawInput, Rect, accesskit::Role, vec2};
|
||||
use figcraft_engine::Session;
|
||||
+1
-1
@@ -1675,7 +1675,7 @@ other key next to `$step` fails the whole batch, which is rolled back. Only an o
|
||||
value too) is read as a reference. A step that is itself a `command.batch` is passed on as written,
|
||||
so the references in its steps are to its own steps, not the outer batch's.
|
||||
|
||||
The end-to-end tests in `crates/mcp/tests/agent_tasks.rs` are worked examples of realistic tasks
|
||||
The end-to-end tests in `crates/mcp/tests/it/agent_tasks.rs` are worked examples of realistic tasks
|
||||
(auto layout, a card built in one batch with step references, components and overrides, gradients
|
||||
and shadows, paint styles, Light/Dark variables, align/distribute/group, boolean subtract + flatten,
|
||||
export, undo/redo, drawing with tools).
|
||||
|
||||
@@ -6,7 +6,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
bash packaging/server/install-test.sh
|
||||
if grep -rniE 'photoshop|psd|photocraft|vectorcraft|illustrator|adobe|figma' packaging/ .github/ \
|
||||
--exclude=packaging-lint.yml --exclude=check-app-specific.sh; then
|
||||
echo "::error::package metadata mentions another product (see above)"
|
||||
echo "::error::package metadata mentions another product (see above)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+16
-10
@@ -24,10 +24,11 @@ workspace_version() {
|
||||
/^\[/ { in_pkg = ($0 == "[workspace.package]") ; next }
|
||||
in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit }
|
||||
' "$ROOT/Cargo.toml"
|
||||
return $?
|
||||
}
|
||||
|
||||
VERSION="${FIGCRAFT_VERSION:-$(workspace_version)}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -37,7 +38,7 @@ DIST="${DIST:-$ROOT/dist/release}"
|
||||
mkdir -p "$DIST"
|
||||
export DIST
|
||||
|
||||
if [ -z "${FIGCRAFT_BUILD_SHA:-}" ]; then
|
||||
if [[ -z "${FIGCRAFT_BUILD_SHA:-}" ]]; then
|
||||
FIGCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)"
|
||||
fi
|
||||
export FIGCRAFT_BUILD_SHA
|
||||
@@ -47,7 +48,7 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
|
||||
# The headless CLI ships next to the app once apps/figcraft-cli exists.
|
||||
BINS=(figcraft)
|
||||
HAS_CLI=0
|
||||
if [ -f "$ROOT/apps/figcraft-cli/Cargo.toml" ]; then
|
||||
if [[ -f "$ROOT/apps/figcraft-cli/Cargo.toml" ]]; then
|
||||
BINS+=(figcraft-cli)
|
||||
HAS_CLI=1
|
||||
fi
|
||||
@@ -58,7 +59,8 @@ unset _bin
|
||||
# The binary used for a `--version` smoke test: the CLI when shipped, else the app (which answers
|
||||
# --version before opening a window).
|
||||
smoke_bin() {
|
||||
if [ "$HAS_CLI" = 1 ]; then echo figcraft-cli; else echo figcraft; fi
|
||||
if [[ "$HAS_CLI" = 1 ]]; then echo figcraft-cli; else echo figcraft; fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Emit a GitHub Actions warning (plain stderr outside Actions).
|
||||
@@ -68,31 +70,33 @@ retry() {
|
||||
local attempt
|
||||
for attempt in 1 2 3 4 5; do
|
||||
"$@" && return 0
|
||||
[ "$attempt" = 5 ] || { echo "retrying ($attempt/5): $*" >&2; sleep 15; }
|
||||
[[ "$attempt" = 5 ]] || { echo "retrying ($attempt/5): $*" >&2; sleep 15; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Copy licence and readme files that exist into a package directory.
|
||||
copy_docs() {
|
||||
local dest="$1" f
|
||||
for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE NOTICE COPYRIGHT; do
|
||||
if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
if [[ -f "$ROOT/$f" ]]; then cp "$ROOT/$f" "$dest/"; fi
|
||||
done
|
||||
copy_font_licences "$dest"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Builds made with craft-fonts (CRAFT_FONTS_DIR, all official releases) embed its fonts, which
|
||||
# are OFL-1.1: ship each font's licence as OFL-<family-dir>.txt. No-op without CRAFT_FONTS_DIR.
|
||||
copy_font_licences() {
|
||||
local dest="$1" lic fam
|
||||
[ -n "${CRAFT_FONTS_DIR:-}" ] || return 0
|
||||
[[ -n "${CRAFT_FONTS_DIR:-}" ]] || return 0
|
||||
for lic in "$CRAFT_FONTS_DIR"/fonts/*/OFL.txt; do
|
||||
[ -f "$lic" ] || continue
|
||||
[[ -f "$lic" ]] || continue
|
||||
fam="$(basename "$(dirname "$lic")")"
|
||||
cp "$lic" "$dest/OFL-$fam.txt"
|
||||
done
|
||||
@@ -100,5 +104,7 @@ copy_font_licences() {
|
||||
|
||||
# Portable SHA-256 of a file (prints just the hash).
|
||||
sha256() {
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null; then sha256sum "$file" | cut -d' ' -f1; else shasum -a 256 "$file" | cut -d' ' -f1; fi
|
||||
return $?
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ APP_ID=ai.storyteller.figcraft
|
||||
|
||||
SKIP_BUILD=0
|
||||
DRY_RUN=0
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--dry-run) DRY_RUN=1; SKIP_BUILD=1; shift ;;
|
||||
@@ -37,7 +37,7 @@ case "$MACHINE" in
|
||||
arm64 | aarch64) ARCH=aarch64 ;;
|
||||
*) echo "unsupported architecture $MACHINE" >&2; exit 2 ;;
|
||||
esac
|
||||
if [ "$DRY_RUN" = 0 ] && [ "$(uname -s)" != FreeBSD ]; then
|
||||
if [[ "$DRY_RUN" = 0 ]] && [[ "$(uname -s)" != FreeBSD ]]; then
|
||||
echo "error: build on FreeBSD (or pass --dry-run to check the tree layout)" >&2
|
||||
exit 2
|
||||
fi
|
||||
@@ -47,7 +47,7 @@ echo "==> FigCraft $VERSION for FreeBSD $ARCH"
|
||||
|
||||
# The release VM has 12 GB; full parallelism on the biggest crates runs it out of memory.
|
||||
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-4}"
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked "${CARGO_PKGS[@]}")
|
||||
fi
|
||||
WORK="$CARGO_TARGET_DIR/freebsd-package"
|
||||
@@ -57,7 +57,7 @@ mkdir -p "$WORK"
|
||||
|
||||
BIN="$CARGO_TARGET_DIR/release"
|
||||
OUT_DIR="$DIST"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
if [[ "$DRY_RUN" = 1 ]]; then
|
||||
# Stub binaries, and the tarball stays in the work dir so dist/ only ever holds real builds.
|
||||
BIN="$WORK/stub-bin"
|
||||
OUT_DIR="$WORK"
|
||||
@@ -86,12 +86,12 @@ sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$FIGCRAFT_BUILD_DATE/g" \
|
||||
"$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml"
|
||||
cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/"
|
||||
copy_docs "$STAGE/share/doc/figcraft"
|
||||
if [ -f "$ROOT/ATTRIBUTION.md" ]; then install -m 644 "$ROOT/ATTRIBUTION.md" "$STAGE/share/doc/figcraft/"; fi
|
||||
if [[ -f "$ROOT/ATTRIBUTION.md" ]]; then install -m 644 "$ROOT/ATTRIBUTION.md" "$STAGE/share/doc/figcraft/"; fi
|
||||
|
||||
for f in "${BINS[@]/#/bin/}" "share/applications/$APP_ID.desktop" \
|
||||
"share/icons/hicolor/256x256/apps/$APP_ID.png" \
|
||||
share/doc/figcraft/LICENSE-MIT share/doc/figcraft/LICENSE-APACHE; do
|
||||
if [ ! -e "$STAGE/$f" ]; then echo "error: $f is missing from the package" >&2; exit 1; fi
|
||||
if [[ ! -e "$STAGE/$f" ]]; then echo "error: $f is missing from the package" >&2; exit 1; fi
|
||||
done
|
||||
|
||||
# ---- .tar.gz ------------------------------------------------------------------------------------
|
||||
@@ -100,7 +100,7 @@ tar -C "$WORK" -czf "$OUT_DIR/$BASENAME.tar.gz" "$BASENAME"
|
||||
echo "wrote $OUT_DIR/$BASENAME.tar.gz"
|
||||
|
||||
"$STAGE/bin/$(smoke_bin)" --version
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
if [[ "$DRY_RUN" = 1 ]]; then
|
||||
echo "==> dry run: tarball contents"
|
||||
tar -tzvf "$OUT_DIR/$BASENAME.tar.gz"
|
||||
fi
|
||||
|
||||
@@ -37,10 +37,10 @@ fail() {
|
||||
|
||||
# 1. Launch works and passes arguments through.
|
||||
out="$("$APPDIR/AppRun" open file.png)" || fail "AppRun exited non-zero"
|
||||
[ "$out" = "stub: open file.png" ] || fail "arguments not forwarded: $out"
|
||||
[[ "$out" = "stub: open file.png" ]] || fail "arguments not forwarded: $out"
|
||||
|
||||
DESKTOP="$XDG_DATA_HOME/applications/$APP_ID.desktop"
|
||||
[ -f "$DESKTOP" ] || fail "desktop entry not installed"
|
||||
[[ -f "$DESKTOP" ]] || fail "desktop entry not installed"
|
||||
|
||||
# 2. Exec rewritten to the AppImage (quoted, spaces intact), TryExec dropped,
|
||||
# everything else preserved from the packaged entry.
|
||||
@@ -55,15 +55,15 @@ fi
|
||||
|
||||
# 3. Icons landed under the name the .desktop entry references.
|
||||
for f in "16x16/apps/$APP_ID.png" "256x256/apps/$APP_ID.png" "scalable/apps/$APP_ID.svg"; do
|
||||
[ -f "$XDG_DATA_HOME/icons/hicolor/$f" ] || fail "icon missing: $f"
|
||||
[[ -f "$XDG_DATA_HOME/icons/hicolor/$f" ]] || fail "icon missing: $f"
|
||||
done
|
||||
|
||||
# 4. Idempotent: a second launch must not rewrite anything (inode unchanged).
|
||||
before="$(stat -c %i "$DESKTOP")"
|
||||
out="$("$APPDIR/AppRun")" || fail "second launch exited non-zero"
|
||||
[ "$out" = "stub: " ] || fail "second launch output: $out"
|
||||
[[ "$out" = "stub: " ]] || fail "second launch output: $out"
|
||||
after="$(stat -c %i "$DESKTOP")"
|
||||
[ "$before" = "$after" ] || fail "desktop entry rewritten although unchanged"
|
||||
[[ "$before" = "$after" ]] || fail "desktop entry rewritten although unchanged"
|
||||
|
||||
# 5. Moving the AppImage refreshes Exec.
|
||||
mv "$APPIMAGE" "$TMP/moved.AppImage"
|
||||
@@ -74,40 +74,40 @@ grep -Fxq "Exec=\"$(readlink -f "$APPIMAGE")\" %U" "$DESKTOP" || fail "Exec not
|
||||
# 6. Opt-out installs nothing.
|
||||
rm -rf "$XDG_DATA_HOME"
|
||||
FIGCRAFT_NO_DESKTOP_INTEGRATION=1 "$APPDIR/AppRun" >/dev/null || fail "opt-out launch exited non-zero"
|
||||
[ ! -e "$XDG_DATA_HOME" ] || fail "opt-out still integrated"
|
||||
[[ ! -e "$XDG_DATA_HOME" ]] || fail "opt-out still integrated"
|
||||
|
||||
# 7. An unwritable data dir never blocks the launch.
|
||||
mkdir -p "$TMP/ro" && chmod a-w "$TMP/ro"
|
||||
out="$(XDG_DATA_HOME="$TMP/ro/data" "$APPDIR/AppRun" out.txt)" || fail "launch blocked by unwritable data dir"
|
||||
[ "$out" = "stub: out.txt" ] || fail "stub not run against unwritable data dir: $out"
|
||||
[[ "$out" = "stub: out.txt" ]] || fail "stub not run against unwritable data dir: $out"
|
||||
chmod u+w "$TMP/ro"
|
||||
|
||||
# 8. No $APPIMAGE (extracted AppDir): launch only, nothing installed.
|
||||
rm -rf "$XDG_DATA_HOME"
|
||||
env -u APPIMAGE "$APPDIR/AppRun" >/dev/null || fail "extracted-AppDir launch exited non-zero"
|
||||
[ ! -e "$XDG_DATA_HOME" ] || fail "integrated without $APPIMAGE"
|
||||
[[ ! -e "$XDG_DATA_HOME" ]] || fail "integrated without $APPIMAGE"
|
||||
|
||||
# 9. A path that would need Exec escaping (here a `$`): launch only, nothing installed.
|
||||
rm -rf "$XDG_DATA_HOME"
|
||||
odd="$TMP/odd\$(id).AppImage"
|
||||
: >"$odd"
|
||||
out="$(APPIMAGE="$odd" "$APPDIR/AppRun" x)" || fail "launch with an odd path exited non-zero"
|
||||
[ "$out" = "stub: x" ] || fail "odd path: $out"
|
||||
[ ! -e "$XDG_DATA_HOME" ] || fail "integrated an AppImage path that needs escaping"
|
||||
[[ "$out" = "stub: x" ]] || fail "odd path: $out"
|
||||
[[ ! -e "$XDG_DATA_HOME" ]] || fail "integrated an AppImage path that needs escaping"
|
||||
|
||||
# 10. `cli <args>` runs figcraft-cli with the remaining arguments and integrates nothing.
|
||||
rm -rf "$XDG_DATA_HOME"
|
||||
out="$("$APPDIR/AppRun" cli mcp --attach 7979 2>&1)" && fail "cli ran without figcraft-cli in the AppDir"
|
||||
[ "$out" = "figcraft: this AppImage was built without figcraft-cli" ] || fail "missing cli: $out"
|
||||
[[ "$out" = "figcraft: this AppImage was built without figcraft-cli" ]] || fail "missing cli: $out"
|
||||
printf '#!/bin/sh\necho "cli stub: $*"\n' >"$APPDIR/usr/bin/figcraft-cli"
|
||||
chmod +x "$APPDIR/usr/bin/figcraft-cli"
|
||||
out="$("$APPDIR/AppRun" cli mcp --attach 7979)" || fail "cli launch exited non-zero"
|
||||
[ "$out" = "cli stub: mcp --attach 7979" ] || fail "cli arguments not forwarded: $out"
|
||||
[[ "$out" = "cli stub: mcp --attach 7979" ]] || fail "cli arguments not forwarded: $out"
|
||||
out="$("$APPDIR/AppRun" cli)" || fail "bare cli launch exited non-zero"
|
||||
[ "$out" = "cli stub: " ] || fail "bare cli output: $out"
|
||||
[ ! -e "$XDG_DATA_HOME" ] || fail "cli launch integrated the desktop"
|
||||
[[ "$out" = "cli stub: " ]] || fail "bare cli output: $out"
|
||||
[[ ! -e "$XDG_DATA_HOME" ]] || fail "cli launch integrated the desktop"
|
||||
# Only the first argument selects the CLI.
|
||||
out="$("$APPDIR/AppRun" file.figcraft cli)" || fail "app launch with a later cli argument exited non-zero"
|
||||
[ "$out" = "stub: file.figcraft cli" ] || fail "later cli argument hijacked the launch: $out"
|
||||
[[ "$out" = "stub: file.figcraft cli" ]] || fail "later cli argument hijacked the launch: $out"
|
||||
|
||||
echo "AppRun desktop integration ok"
|
||||
|
||||
@@ -22,7 +22,7 @@ FLATHUB=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
TEST=1
|
||||
TARBALL=""
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-test) TEST=0; shift ;;
|
||||
-h | --help) sed -n '2,14p' "$0"; exit 0 ;;
|
||||
@@ -39,7 +39,7 @@ case "$ARCH" in
|
||||
esac
|
||||
BASENAME="figcraft-$VERSION-linux-$ARCH"
|
||||
TARBALL="${TARBALL:-$DIST/$BASENAME.tar.gz}"
|
||||
[ -f "$TARBALL" ] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL not found (run packaging/linux/package.sh --formats tar first)" >&2; exit 1; }
|
||||
for tool in flatpak flatpak-builder; do
|
||||
command -v "$tool" >/dev/null || { echo "error: $tool not found (apt install flatpak flatpak-builder librsvg2-common)" >&2; exit 1; }
|
||||
done
|
||||
@@ -50,7 +50,7 @@ WORK="$CARGO_TARGET_DIR/flatpak-bundle"
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK/stage"
|
||||
tar -xzf "$TARBALL" -C "$WORK/stage" --strip-components=1
|
||||
[ -x "$WORK/stage/bin/figcraft" ] || { echo "error: $TARBALL has no bin/figcraft" >&2; exit 1; }
|
||||
[[ -x "$WORK/stage/bin/figcraft" ]] || { echo "error: $TARBALL has no bin/figcraft" >&2; exit 1; }
|
||||
cp "$HERE/flatpak/$APP_ID.bundle.yml" "$WORK/$APP_ID.yml"
|
||||
|
||||
flatpak remote-add --user --if-not-exists flathub "$FLATHUB"
|
||||
@@ -63,10 +63,10 @@ OUT="$DIST/$BASENAME.flatpak"
|
||||
flatpak build-bundle --runtime-repo="$FLATHUB" "$WORK/repo" "$OUT" "$APP_ID" stable
|
||||
echo "wrote $OUT"
|
||||
|
||||
if [ "$TEST" = 1 ]; then
|
||||
if [[ "$TEST" = 1 ]]; then
|
||||
flatpak install --user -y --noninteractive --reinstall "$OUT"
|
||||
flatpak info --user "$APP_ID"
|
||||
if [ -x "$WORK/stage/bin/figcraft-cli" ]; then smoke=figcraft-cli; else smoke=figcraft; fi
|
||||
if [[ -x "$WORK/stage/bin/figcraft-cli" ]]; then smoke=figcraft-cli; else smoke=figcraft; fi
|
||||
flatpak run --command="$smoke" "$APP_ID" --version
|
||||
flatpak run --command=sh "$APP_ID" -c 'ls /app/share/applications /app/share/metainfo /app/share/mime/packages /app/share/icons/hicolor/256x256/apps'
|
||||
fi
|
||||
|
||||
+49
-13
@@ -27,7 +27,7 @@ APP_ID=ai.storyteller.figcraft
|
||||
|
||||
SKIP_BUILD=0
|
||||
FORMATS="appimage deb rpm tar"
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
--formats) FORMATS="$2"; shift 2 ;;
|
||||
@@ -48,7 +48,7 @@ BASENAME="figcraft-$VERSION-linux-$ARCH"
|
||||
|
||||
echo "==> FigCraft $VERSION for Linux $ARCH ($FORMATS; ${BINS[*]})"
|
||||
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked "${CARGO_PKGS[@]}" ${CROSS_TARGET:+--target "$CROSS_TARGET"})
|
||||
fi
|
||||
BIN="$CARGO_TARGET_DIR/${CROSS_TARGET:+$CROSS_TARGET/}release"
|
||||
@@ -78,7 +78,7 @@ if command -v appstreamcli >/dev/null; then
|
||||
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
|
||||
fi
|
||||
|
||||
has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
has() { local format="$1"; case " $FORMATS " in *" $format "*) return 0 ;; *) return 1 ;; esac; }
|
||||
|
||||
# ---- .tar.gz ------------------------------------------------------------------------------------
|
||||
if has tar; then
|
||||
@@ -96,7 +96,7 @@ if has deb || has rpm; then
|
||||
# nfpm expands env vars in fields like `version` and `arch`, but not in `contents[].src`.
|
||||
# Without the CLI, drop its `#cli` lines.
|
||||
cli_filter='/#cli$/d'
|
||||
[ "$HAS_CLI" = 1 ] && cli_filter='s/ #cli$//'
|
||||
[[ "$HAS_CLI" = 1 ]] && cli_filter='s/ #cli$//'
|
||||
sed -e "s|\${STAGE}|$STAGE|g" -e "$cli_filter" "$HERE/nfpm.yaml" >"$WORK/nfpm.yaml"
|
||||
for fmt in deb rpm; do
|
||||
if has "$fmt"; then (cd "$ROOT" && nfpm package -f "$WORK/nfpm.yaml" -p "$fmt" -t "$DIST/$BASENAME.$fmt"); fi
|
||||
@@ -105,6 +105,23 @@ fi
|
||||
|
||||
# ---- AppImage -----------------------------------------------------------------------------------
|
||||
if has appimage; then
|
||||
APPIMAGETOOL_VERSION=1.9.1
|
||||
case "$ARCH" in
|
||||
x86_64) APPIMAGETOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 ;;
|
||||
aarch64) APPIMAGETOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 ;;
|
||||
*) APPIMAGETOOL_SHA256='' ;;
|
||||
esac
|
||||
# appimagetool otherwise fetches the unpinned type2-runtime `continuous` build and embeds it.
|
||||
APPIMAGE_RUNTIME_VERSION=20251108
|
||||
case "$ARCH" in
|
||||
x86_64) APPIMAGE_RUNTIME_SHA256=2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d ;;
|
||||
aarch64) APPIMAGE_RUNTIME_SHA256=00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444 ;;
|
||||
*) APPIMAGE_RUNTIME_SHA256='' ;;
|
||||
esac
|
||||
if [[ -z "$APPIMAGE_RUNTIME_SHA256" ]]; then
|
||||
echo "no pinned AppImage runtime for $ARCH; build the AppImage on x86_64 or aarch64" >&2
|
||||
exit 2
|
||||
fi
|
||||
APPDIR="$WORK/FigCraft.AppDir"
|
||||
cp -R "$STAGE" "$APPDIR"
|
||||
mv "$APPDIR/usr/share/doc" "$WORK/doc-unused"
|
||||
@@ -118,18 +135,37 @@ if has appimage; then
|
||||
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
|
||||
|
||||
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
||||
if [ -z "$TOOL" ]; then
|
||||
TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage"
|
||||
if [ ! -x "$TOOL" ]; then
|
||||
curl --proto '=https' -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage"
|
||||
chmod +x "$TOOL"
|
||||
if [[ -z "$TOOL" ]]; then
|
||||
if [[ -z "$APPIMAGETOOL_SHA256" ]]; then
|
||||
echo "no pinned appimagetool for $ARCH; set APPIMAGETOOL or put appimagetool on PATH" >&2
|
||||
exit 2
|
||||
fi
|
||||
TOOL="$CARGO_TARGET_DIR/appimagetool-$APPIMAGETOOL_VERSION-$ARCH.AppImage"
|
||||
if [[ ! -x "$TOOL" ]]; then
|
||||
download="$TOOL.download"
|
||||
curl --proto '=https' -fsSL -o "$download" \
|
||||
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
|
||||
chmod 755 "$download"
|
||||
mv "$download" "$TOOL"
|
||||
fi
|
||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
|
||||
fi
|
||||
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
||||
if [[ ! -f "$RUNTIME" ]]; then
|
||||
download="$RUNTIME.download"
|
||||
curl --proto '=https' -fsSL -o "$download" \
|
||||
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
|
||||
mv "$download" "$RUNTIME"
|
||||
fi
|
||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
|
||||
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
||||
# relative, e.g. target/agent-<name>).
|
||||
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
||||
APPDIR="$(cd "$APPDIR" && pwd)"
|
||||
TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
|
||||
RUNTIME="$(cd "$(dirname "$RUNTIME")" && pwd)/$(basename "$RUNTIME")"
|
||||
# A .zsync left by an earlier run would hide a missing zsyncmake and describe another file.
|
||||
rm -f "$OUT.zsync"
|
||||
# Update information: AppImageUpdate, AppImageLauncher and the like read it from the
|
||||
@@ -141,18 +177,18 @@ if has appimage; then
|
||||
# Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime,
|
||||
# so users don't need libfuse2 either. With zsyncmake on the host (CI installs the zsync
|
||||
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
||||
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
||||
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream --runtime-file "$RUNTIME" -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
||||
echo "wrote $OUT"
|
||||
if [ -s "$OUT.zsync" ]; then
|
||||
if [[ -s "$OUT.zsync" ]]; then
|
||||
echo "wrote $OUT.zsync"
|
||||
else
|
||||
warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${CROSS_TARGET:-}" ]; then
|
||||
if [[ -z "${CROSS_TARGET:-}" ]]; then
|
||||
"$STAGE/usr/bin/$(smoke_bin)" --version
|
||||
elif [ -n "${EMULATOR:-}" ]; then
|
||||
elif [[ -n "${EMULATOR:-}" ]]; then
|
||||
"$EMULATOR" "$STAGE/usr/bin/$(smoke_bin)" --version
|
||||
fi
|
||||
echo "==> done"
|
||||
|
||||
@@ -12,7 +12,7 @@ umask 077
|
||||
# shellcheck source=../env.sh
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
|
||||
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
|
||||
if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
|
||||
warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized"
|
||||
exit 0
|
||||
fi
|
||||
@@ -36,13 +36,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS"
|
||||
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
|
||||
|
||||
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')"
|
||||
if [ -z "$IDENTITY" ]; then
|
||||
if [[ -z "$IDENTITY" ]]; then
|
||||
security find-identity -v -p codesigning "$KEYCHAIN" >&2
|
||||
echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Imported signing identity $IDENTITY"
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
if [[ -n "${GITHUB_ENV:-}" ]]; then
|
||||
{ echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV"
|
||||
else
|
||||
echo "MACOS_SIGN_IDENTITY=$IDENTITY"
|
||||
|
||||
+13
-13
@@ -19,7 +19,7 @@ HERE="$ROOT/packaging/macos"
|
||||
|
||||
ARCH=universal
|
||||
SKIP_BUILD=0
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--skip-build) SKIP_BUILD=1; shift ;;
|
||||
@@ -44,9 +44,9 @@ DMG="$DIST/figcraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/figcraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
|
||||
NOTARIZE=0
|
||||
if [ "$IDENTITY" = "-" ]; then
|
||||
if [[ "$IDENTITY" = "-" ]]; then
|
||||
warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized"
|
||||
elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
elif [[ -n "${APPLE_ID:-}" ]] && [[ -n "${APPLE_PASSWORD:-}" ]] && [[ -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
NOTARIZE=1
|
||||
else
|
||||
warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized"
|
||||
@@ -55,7 +55,7 @@ fi
|
||||
echo "==> FigCraft $VERSION for macOS ($ARCH; ${BINS[*]}), identity: $IDENTITY, notarize: $NOTARIZE"
|
||||
|
||||
# ---- build -------------------------------------------------------------------------------------
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
args=()
|
||||
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
||||
(cd "$ROOT" && cargo build --release --locked "${CARGO_PKGS[@]}" "${args[@]}")
|
||||
@@ -74,9 +74,9 @@ done
|
||||
sign() {
|
||||
# Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped.
|
||||
local ts=(--timestamp)
|
||||
[ "$IDENTITY" = "-" ] && ts=(--timestamp=none)
|
||||
[[ "$IDENTITY" = "-" ]] && ts=(--timestamp=none)
|
||||
local kc=()
|
||||
[ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
[[ -n "${MACOS_KEYCHAIN:-}" ]] && kc=(--keychain "$MACOS_KEYCHAIN")
|
||||
codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@"
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ notarize() {
|
||||
echo "$out"
|
||||
id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)"
|
||||
status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)"
|
||||
if [ "$status" != "Accepted" ]; then
|
||||
if [ -n "$id" ]; then
|
||||
if [[ "$status" != "Accepted" ]]; then
|
||||
if [[ -n "$id" ]]; then
|
||||
xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true
|
||||
fi
|
||||
echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2
|
||||
@@ -116,7 +116,7 @@ mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
||||
cp "$WORK/bin/figcraft" "$APP/Contents/MacOS/FigCraft"
|
||||
cp "$WORK/FigCraft.icns" "$APP/Contents/Resources/FigCraft.icns"
|
||||
# Licences of the embedded craft-fonts fonts (only when built with CRAFT_FONTS_DIR).
|
||||
if [ -n "${CRAFT_FONTS_DIR:-}" ]; then
|
||||
if [[ -n "${CRAFT_FONTS_DIR:-}" ]]; then
|
||||
mkdir -p "$APP/Contents/Resources/Licenses"
|
||||
copy_font_licences "$APP/Contents/Resources/Licenses"
|
||||
fi
|
||||
@@ -132,7 +132,7 @@ sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/
|
||||
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP"
|
||||
codesign --verify --strict --deep --verbose=2 "$APP"
|
||||
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
ditto -c -k --keepParent "$APP" "$WORK/FigCraft-notarize.zip"
|
||||
notarize "$WORK/FigCraft-notarize.zip"
|
||||
retry xcrun stapler staple "$APP"
|
||||
@@ -154,7 +154,7 @@ hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
||||
rm -f "$WORK/raw.dmg"
|
||||
sign "$DMG"
|
||||
codesign --verify --strict --verbose=2 "$DMG"
|
||||
if [ "$NOTARIZE" = 1 ]; then
|
||||
if [[ "$NOTARIZE" = 1 ]]; then
|
||||
notarize "$DMG"
|
||||
retry xcrun stapler staple "$DMG"
|
||||
retry xcrun stapler validate "$DMG"
|
||||
@@ -162,7 +162,7 @@ if [ "$NOTARIZE" = 1 ]; then
|
||||
fi
|
||||
|
||||
# ---- CLI ---------------------------------------------------------------------------------------
|
||||
if [ "$HAS_CLI" = 1 ]; then
|
||||
if [[ "$HAS_CLI" = 1 ]]; then
|
||||
echo "==> building $CLI_ZIP"
|
||||
CLI_DIR="$WORK/figcraft-cli-$VERSION-macos-$ARCH"
|
||||
mkdir -p "$CLI_DIR"
|
||||
@@ -176,7 +176,7 @@ if [ "$HAS_CLI" = 1 ]; then
|
||||
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
|
||||
# notarytool accepts a zip of the bare binary. A bare Mach-O can't carry a stapled ticket, so
|
||||
# Gatekeeper looks the notarization up online when a quarantined copy first runs.
|
||||
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
|
||||
if [[ "$NOTARIZE" = 1 ]]; then notarize "$CLI_ZIP"; fi
|
||||
"$WORK/bin/figcraft-cli" --version
|
||||
else
|
||||
warn "apps/figcraft-cli is not in this workspace: no CLI zip"
|
||||
|
||||
+15
-15
@@ -23,7 +23,7 @@ set -euo pipefail
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
|
||||
ARCH=universal
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
-h | --help) sed -n '2,20p' "$0"; exit 0 ;;
|
||||
@@ -41,13 +41,13 @@ DMG="$DIST/figcraft-$VERSION-macos-$ARCH.dmg"
|
||||
CLI_ZIP="$DIST/figcraft-cli-$VERSION-macos-$ARCH.zip"
|
||||
IDENTITY="${MACOS_SIGN_IDENTITY:--}"
|
||||
STRICT=0
|
||||
if [ "$IDENTITY" != "-" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
if [[ "$IDENTITY" != "-" ]] && [[ -n "${APPLE_ID:-}" ]] && [[ -n "${APPLE_PASSWORD:-}" ]] && [[ -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
STRICT=1
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
fail() {
|
||||
if [ "$STRICT" = 1 ]; then
|
||||
if [[ "$STRICT" = 1 ]]; then
|
||||
echo "error: $*" >&2
|
||||
FAILED=1
|
||||
else
|
||||
@@ -58,22 +58,22 @@ fail() {
|
||||
# spctl's online ticket lookup can lag a few seconds behind notarytool's "Accepted".
|
||||
assess() {
|
||||
local out attempt tries=1
|
||||
[ "$STRICT" = 1 ] && tries=5
|
||||
[[ "$STRICT" = 1 ]] && tries=5
|
||||
for attempt in $(seq "$tries"); do
|
||||
if out="$(spctl "$@" 2>&1)"; then
|
||||
printf '%s\n' "$out"
|
||||
return 0
|
||||
fi
|
||||
[ "$attempt" = "$tries" ] || sleep 10
|
||||
[[ "$attempt" = "$tries" ]] || sleep 10
|
||||
done
|
||||
printf '%s\n' "$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
FILES=("$DMG")
|
||||
if [ "$HAS_CLI" = 1 ]; then FILES+=("$CLI_ZIP"); fi
|
||||
if [[ "$HAS_CLI" = 1 ]]; then FILES+=("$CLI_ZIP"); fi
|
||||
for f in "${FILES[@]}"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "error: missing $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -83,11 +83,11 @@ WORK="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/figcraft-verify.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# ---- CLI zip (when apps/figcraft-cli exists) ----------------------------------------------------
|
||||
if [ "$HAS_CLI" = 1 ]; then
|
||||
if [[ "$HAS_CLI" = 1 ]]; then
|
||||
echo "==> $(basename "$CLI_ZIP")"
|
||||
ditto -x -k "$CLI_ZIP" "$WORK/cli"
|
||||
CLI="$WORK/cli/figcraft-cli-$VERSION-macos-$ARCH/figcraft-cli"
|
||||
if [ ! -x "$CLI" ]; then
|
||||
if [[ ! -x "$CLI" ]]; then
|
||||
echo "error: $(basename "$CLI_ZIP") has no executable figcraft-cli-$VERSION-macos-$ARCH/figcraft-cli" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -111,7 +111,7 @@ if [ "$HAS_CLI" = 1 ]; then
|
||||
|| fail "figcraft-cli is not signed with a Developer ID Application identity"
|
||||
printf '%s\n' "$details" | grep -q '^Timestamp=' \
|
||||
|| fail "figcraft-cli signature has no secure timestamp"
|
||||
if [ -n "${APPLE_TEAM_ID:-}" ]; then
|
||||
if [[ -n "${APPLE_TEAM_ID:-}" ]]; then
|
||||
printf '%s\n' "$details" | grep -qx "TeamIdentifier=$APPLE_TEAM_ID" \
|
||||
|| fail "figcraft-cli is signed by a different team"
|
||||
fi
|
||||
@@ -129,25 +129,25 @@ fi
|
||||
# ---- DMG ---------------------------------------------------------------------------------------
|
||||
echo "==> $(basename "$DMG")"
|
||||
codesign --verify --strict --verbose=2 "$DMG"
|
||||
if [ "$STRICT" = 1 ]; then
|
||||
if [[ "$STRICT" = 1 ]]; then
|
||||
retry xcrun stapler validate "$DMG" || fail "no notarization ticket stapled to the DMG"
|
||||
assess --assess --type open --context context:primary-signature -vv "$DMG" \
|
||||
|| fail "Gatekeeper rejects the DMG"
|
||||
fi
|
||||
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
if [[ "$FAILED" != 0 ]]; then
|
||||
echo "error: macOS artifacts failed verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$STRICT" = 1 ]; then
|
||||
if [[ "$STRICT" = 1 ]]; then
|
||||
result="signed (Developer ID, hardened runtime), notarized; DMG stapled"
|
||||
else
|
||||
result="signature intact; Developer ID and notarization not required (no signing secrets)"
|
||||
fi
|
||||
echo "==> verified: $result"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||
names="\`$(basename "$DMG")\`"
|
||||
if [ "$HAS_CLI" = 1 ]; then names="$names, \`$(basename "$CLI_ZIP")\`"; fi
|
||||
if [[ "$HAS_CLI" = 1 ]]; then names="$names, \`$(basename "$CLI_ZIP")\`"; fi
|
||||
echo "- macOS $names: $result" >>"$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
@@ -10,7 +10,7 @@ WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
FAILED=0
|
||||
|
||||
main() { bash "$HERE/install.sh" --caddy-main "$1" ops@example.com; }
|
||||
main() { local caddyfile="$1"; bash "$HERE/install.sh" --caddy-main "$caddyfile" ops@example.com; }
|
||||
fail() { printf 'FAIL: %s\n' "$*" >&2; FAILED=1; }
|
||||
|
||||
# Missing: ours.
|
||||
@@ -28,7 +28,7 @@ printf 'shop.example.com {\n\treverse_proxy 127.0.0.1:8080\n}\n' >"$WORK/theirs"
|
||||
cp "$WORK/theirs" "$WORK/run1"
|
||||
main "$WORK/run1" >"$WORK/run2"
|
||||
grep -q 'shop.example.com' "$WORK/run2" || fail "the operator's site is kept"
|
||||
[ "$(grep -c 'import figcraft-server.caddy' "$WORK/run2")" = 1 ] || fail "one import line is added"
|
||||
[[ "$(grep -c 'import figcraft-server.caddy' "$WORK/run2")" = 1 ]] || fail "one import line is added"
|
||||
main "$WORK/run2" >"$WORK/run3"
|
||||
cmp -s "$WORK/run2" "$WORK/run3" || fail "running again leaves a Caddyfile with other sites as it is"
|
||||
main "$WORK/run3" >"$WORK/run4"
|
||||
@@ -39,5 +39,5 @@ main "$WORK/none" >"$WORK/ours1"
|
||||
main "$WORK/ours1" >"$WORK/ours2"
|
||||
cmp -s "$WORK/ours1" "$WORK/ours2" || fail "our own Caddyfile stays the same"
|
||||
|
||||
if [ "$FAILED" = 0 ]; then echo "install.sh Caddyfile handling: ok"; fi
|
||||
if [[ "$FAILED" = 0 ]]; then echo "install.sh Caddyfile handling: ok"; fi
|
||||
exit "$FAILED"
|
||||
|
||||
+44
-40
@@ -41,7 +41,7 @@ SERVICE=figcraft-server
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# The source tree this script sits in (packaging/server/ of a copy or checkout), if any.
|
||||
LOCAL_SRC=""
|
||||
if [ -f "$HERE/../../Cargo.toml" ] && [ -d "$HERE/../../apps/figcraft-server" ]; then
|
||||
if [[ -f "$HERE/../../Cargo.toml" ]] && [[ -d "$HERE/../../apps/figcraft-server" ]]; then
|
||||
LOCAL_SRC="$(cd "$HERE/../.." && pwd)"
|
||||
fi
|
||||
|
||||
@@ -64,9 +64,9 @@ MARK="# figcraft-server (install.sh)"
|
||||
|
||||
caddy_main() { # caddy_main FILE EMAIL: the main Caddyfile to install, on stdout
|
||||
local file="$1" email="$2"
|
||||
if [ -f "$file" ] && grep -qE '^[[:space:]]*import[[:space:]]+figcraft-server\.caddy[[:space:]]*$' "$file"; then
|
||||
if [[ -f "$file" ]] && grep -qE '^[[:space:]]*import[[:space:]]+figcraft-server\.caddy[[:space:]]*$' "$file"; then
|
||||
cat "$file" # it imports our site already: kept exactly as it is
|
||||
elif [ ! -f "$file" ] || ! grep -qvE '^[[:space:]]*(#|$|:80|\{|\}|root|file_server)' "$file"; then
|
||||
elif [[ ! -f "$file" ]] || ! grep -qvE '^[[:space:]]*(#|$|:80|\{|\}|root|file_server)' "$file"; then
|
||||
# Missing, or only the example site the package ships: ours, with the certificate email.
|
||||
printf '%s\n{\n\temail %s\n}\n\nimport figcraft-server.caddy\n' "$MARK" "$email"
|
||||
else
|
||||
@@ -76,7 +76,7 @@ caddy_main() { # caddy_main FILE EMAIL: the main Caddyfile to install, on stdout
|
||||
fi
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
# For tests (packaging/server/install-test.sh): print the Caddyfile this script would write.
|
||||
--caddy-main) caddy_main "${2:-}" "${3:-}"; exit 0 ;;
|
||||
@@ -95,7 +95,7 @@ while [ $# -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "run it as root (sudo ./install.sh ...)"
|
||||
[[ "$(id -u)" -eq 0 ]] || die "run it as root (sudo ./install.sh ...)"
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
case " ${ID:-} ${ID_LIKE:-} " in
|
||||
@@ -104,9 +104,9 @@ case " ${ID:-} ${ID_LIKE:-} " in
|
||||
esac
|
||||
|
||||
ask() { # ask VAR "question" FLAG: read an answer from the terminal when the value wasn't given
|
||||
local var="$1" question="$2" answer
|
||||
if [ -z "${!var}" ]; then
|
||||
[ -t 0 ] || die "give $3 (no terminal to ask on)"
|
||||
local var="$1" question="$2" flag="$3" answer
|
||||
if [[ -z "${!var}" ]]; then
|
||||
[[ -t 0 ]] || die "give $flag (no terminal to ask on)"
|
||||
read -r -p "$question " answer
|
||||
printf -v "$var" '%s' "$answer"
|
||||
fi
|
||||
@@ -115,6 +115,7 @@ ask HOST "DNS name people will use (e.g. figcraft.example.com):" "--host"
|
||||
ask EMAIL "Email for Let's Encrypt and the abuse contact shown to users:" "--email"
|
||||
case "$HOST" in
|
||||
"" | *[!A-Za-z0-9.-]* | .* | *. ) die "--host must be a DNS name like figcraft.example.com" ;;
|
||||
*) ;;
|
||||
esac
|
||||
case "$EMAIL" in
|
||||
*@*.*) ;;
|
||||
@@ -126,6 +127,7 @@ case "$TERMS" in
|
||||
esac
|
||||
case "$EMAIL$TERMS" in
|
||||
*[[:space:]\"\\]*) die "--email and --terms can't contain spaces, quotes or backslashes" ;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
@@ -138,6 +140,7 @@ ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
amd64) ARCH=x86_64 ;;
|
||||
arm64) ARCH=aarch64 ;;
|
||||
*) ;; # already a release name (x86_64, aarch64)
|
||||
esac
|
||||
|
||||
# --- The binary -----------------------------------------------------------------------------
|
||||
@@ -146,9 +149,9 @@ FILES="$HERE" # where the units and the Caddyfile come from
|
||||
|
||||
from_release() { # download and check the release archive; sets NEW_BIN and FILES
|
||||
local version="$VERSION" name dir
|
||||
if [ -z "$version" ]; then
|
||||
if [[ -z "$version" ]]; then
|
||||
version="$(curl --proto '=https' -fsSL "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' | head -n 1)" || return 1
|
||||
[ -n "$version" ] || return 1
|
||||
[[ -n "$version" ]] || return 1
|
||||
fi
|
||||
name="figcraft-server-$version-linux-$ARCH"
|
||||
dir="$WORK/release"
|
||||
@@ -165,7 +168,7 @@ from_release() { # download and check the release archive; sets NEW_BIN and FILE
|
||||
|
||||
SWAP=""
|
||||
cleanup() {
|
||||
if [ -n "$SWAP" ]; then
|
||||
if [[ -n "$SWAP" ]]; then
|
||||
swapoff "$SWAP" 2>/dev/null || true
|
||||
rm -f "$SWAP"
|
||||
fi
|
||||
@@ -174,7 +177,7 @@ trap cleanup EXIT
|
||||
|
||||
as_builder() { # run a command as the unprivileged build user, with its own toolchain
|
||||
local jobs=()
|
||||
if [ -n "${CARGO_BUILD_JOBS:-}" ]; then jobs=("CARGO_BUILD_JOBS=$CARGO_BUILD_JOBS"); fi
|
||||
if [[ -n "${CARGO_BUILD_JOBS:-}" ]]; then jobs=("CARGO_BUILD_JOBS=$CARGO_BUILD_JOBS"); fi
|
||||
runuser -u "$BUILDER" -- env HOME="$BUILD" RUSTUP_HOME="$BUILD/rustup" CARGO_HOME="$BUILD/cargo" \
|
||||
CARGO_TARGET_DIR="$BUILD/target" "${jobs[@]}" "$@"
|
||||
}
|
||||
@@ -184,27 +187,27 @@ from_source() { # build the source tree around this script, else a fresh clone;
|
||||
apt-get install -y -qq build-essential git pkg-config >/dev/null
|
||||
id -u "$BUILDER" >/dev/null 2>&1 || useradd --system --home-dir "$BUILD" --shell /usr/sbin/nologin "$BUILDER"
|
||||
install -d -o "$BUILDER" -g "$BUILDER" -m 755 "$BUILD"
|
||||
if [ ! -x "$BUILD/cargo/bin/cargo" ]; then
|
||||
if [[ ! -x "$BUILD/cargo/bin/cargo" ]]; then
|
||||
as_builder sh -c "curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable --no-modify-path" >/dev/null
|
||||
else
|
||||
as_builder "$BUILD/cargo/bin/rustup" update stable >/dev/null
|
||||
fi
|
||||
# The source root installs files from is root's own (the copy beside this script, or root's
|
||||
# clone); the build user only ever builds a copy of it.
|
||||
if [ -n "$LOCAL_SRC" ]; then
|
||||
if [[ -n "$LOCAL_SRC" ]]; then
|
||||
src="$LOCAL_SRC"
|
||||
say "Building the source in $LOCAL_SRC: this takes a while"
|
||||
else
|
||||
src="$WORK/src"
|
||||
if [ -d "$src/.git" ]; then
|
||||
if [[ -d "$src/.git" ]]; then
|
||||
git -C "$src" fetch --quiet --tags origin
|
||||
else
|
||||
git clone --quiet "https://github.com/$REPO" "$src" ||
|
||||
die "can't clone $REPO (a private repository: copy the source to this machine and run its packaging/server/install.sh)"
|
||||
fi
|
||||
if [ -z "$ref" ]; then
|
||||
if [ -n "$VERSION" ]; then ref="v$VERSION"; else ref="$(git -C "$src" tag --list 'v*' --sort=-v:refname | head -n 1)"; fi
|
||||
[ -n "$ref" ] || ref=main
|
||||
if [[ -z "$ref" ]]; then
|
||||
if [[ -n "$VERSION" ]]; then ref="v$VERSION"; else ref="$(git -C "$src" tag --list 'v*' --sort=-v:refname | head -n 1)"; fi
|
||||
[[ -n "$ref" ]] || ref=main
|
||||
fi
|
||||
say "Building $ref from a clone: this takes a while"
|
||||
# A branch as it is upstream now, or a tag.
|
||||
@@ -216,7 +219,7 @@ from_source() { # build the source tree around this script, else a fresh clone;
|
||||
tar -C "$src" --exclude=./target --exclude=./plugins/target --exclude=./.git -cf - . | as_builder tar -C "$copy" -xf -
|
||||
# The compiler needs about 2 GB: add a temporary swap file on smaller machines.
|
||||
mem_kb="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)"
|
||||
if [ "$mem_kb" -lt 2000000 ] && [ -z "$(swapon --show --noheadings)" ]; then
|
||||
if [[ "$mem_kb" -lt 2000000 ]] && [[ -z "$(swapon --show --noheadings)" ]]; then
|
||||
SWAP="$WORK/swapfile"
|
||||
say "Adding a temporary 2 GB swap file for the build"
|
||||
fallocate -l 2G "$SWAP" 2>/dev/null || dd if=/dev/zero of="$SWAP" bs=1M count=2048 status=none
|
||||
@@ -237,19 +240,19 @@ from_source() { # build the source tree around this script, else a fresh clone;
|
||||
FILES="$src/packaging/server"
|
||||
}
|
||||
|
||||
if [ -x "$HERE/figcraft-server" ] && [ "$FROM_SOURCE" = false ]; then
|
||||
if [[ -x "$HERE/figcraft-server" ]] && [[ "$FROM_SOURCE" = false ]]; then
|
||||
NEW_BIN="$HERE/figcraft-server" # run from an unpacked release archive
|
||||
elif [ -n "$LOCAL_SRC" ] && [ -z "$VERSION" ]; then
|
||||
elif [[ -n "$LOCAL_SRC" ]] && [[ -z "$VERSION" ]]; then
|
||||
from_source # the source copied to (or checked out on) this machine, as it is
|
||||
elif [ "$FROM_SOURCE" = true ] || ! from_release; then
|
||||
[ "$FROM_SOURCE" = true ] || say "No release archive for linux-$ARCH: building from source instead"
|
||||
elif [[ "$FROM_SOURCE" = true ]] || ! from_release; then
|
||||
[[ "$FROM_SOURCE" = true ]] || say "No release archive for linux-$ARCH: building from source instead"
|
||||
from_source
|
||||
fi
|
||||
[ -x "$NEW_BIN" ] || die "no figcraft-server binary to install"
|
||||
[[ -x "$NEW_BIN" ]] || die "no figcraft-server binary to install"
|
||||
# The units and the Caddyfile beside this script when it has them (a checkout or an archive).
|
||||
[ -f "$HERE/figcraft-backup.timer" ] && FILES="$HERE"
|
||||
[[ -f "$HERE/figcraft-backup.timer" ]] && FILES="$HERE"
|
||||
for f in figcraft-server.service Caddyfile figcraft-backup.service figcraft-backup.timer; do
|
||||
[ -f "$FILES/$f" ] || die "$FILES/$f is missing"
|
||||
[[ -f "$FILES/$f" ]] || die "$FILES/$f is missing"
|
||||
done
|
||||
|
||||
# The server's own account runs the binary, from here on too (root never runs what was built).
|
||||
@@ -267,7 +270,7 @@ fi
|
||||
# --- Data and settings ----------------------------------------------------------------------
|
||||
install -d -o figcraft -g figcraft -m 700 "$DATA"
|
||||
install -d -m 755 "$CONFIG_DIR"
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
if [[ ! -f "$CONFIG" ]]; then
|
||||
say "Writing $CONFIG"
|
||||
{
|
||||
printf '# figcraft-server settings (docs/server.md, "Configuration"). install.sh never overwrites this file.\n'
|
||||
@@ -303,8 +306,9 @@ as_server "$BIN" --config "$CONFIG" check >/dev/null ||
|
||||
die "$CONFIG isn't right for this figcraft-server (see above; a release older than open hosting needs --from-source)"
|
||||
|
||||
install_file() { # install_file SOURCE TARGET MODE: true when it changed
|
||||
if cmp -s "$1" "$2"; then return 1; fi
|
||||
install -m "$3" "$1" "$2"
|
||||
local source="$1" target="$2" mode="$3"
|
||||
if cmp -s "$source" "$target"; then return 1; fi
|
||||
install -m "$mode" "$source" "$target"
|
||||
}
|
||||
|
||||
# --- systemd --------------------------------------------------------------------------------
|
||||
@@ -312,16 +316,16 @@ UNITS=false
|
||||
install_file "$FILES/figcraft-server.service" /etc/systemd/system/figcraft-server.service 644 && UNITS=true
|
||||
install_file "$FILES/figcraft-backup.service" /etc/systemd/system/figcraft-backup.service 644 && UNITS=true
|
||||
install_file "$FILES/figcraft-backup.timer" /etc/systemd/system/figcraft-backup.timer 644 && UNITS=true
|
||||
if [ "$UNITS" = true ]; then
|
||||
if [[ "$UNITS" = true ]]; then
|
||||
systemctl daemon-reload
|
||||
CHANGED=true
|
||||
fi
|
||||
systemctl enable --quiet "$SERVICE"
|
||||
if [ "$CHANGED" = true ] || ! systemctl is-active --quiet "$SERVICE"; then
|
||||
if [[ "$CHANGED" = true ]] || ! systemctl is-active --quiet "$SERVICE"; then
|
||||
say "Starting figcraft-server"
|
||||
systemctl restart "$SERVICE"
|
||||
fi
|
||||
if [ "$BACKUPS" = true ]; then
|
||||
if [[ "$BACKUPS" = true ]]; then
|
||||
systemctl enable --now --quiet figcraft-backup.timer
|
||||
fi
|
||||
|
||||
@@ -339,15 +343,15 @@ MAIN="$(mktemp)"
|
||||
caddy_main "$CADDYFILE" "$EMAIL" >"$MAIN"
|
||||
CADDY_CHANGED=false
|
||||
for f in "$CADDYFILE" "$SITE_FILE"; do
|
||||
if [ -f "$f" ]; then cp -p "$f" "$f.before-figcraft"; else rm -f "$f.before-figcraft"; fi
|
||||
if [[ -f "$f" ]]; then cp -p "$f" "$f.before-figcraft"; else rm -f "$f.before-figcraft"; fi
|
||||
done
|
||||
install_file "$SITE" "$SITE_FILE" 644 && CADDY_CHANGED=true
|
||||
install_file "$MAIN" "$CADDYFILE" 644 && CADDY_CHANGED=true
|
||||
rm -f "$SITE" "$MAIN"
|
||||
if [ "$CADDY_CHANGED" = true ]; then
|
||||
if [[ "$CADDY_CHANGED" = true ]]; then
|
||||
if ! caddy validate --config "$CADDYFILE" --adapter caddyfile >/dev/null 2>&1; then
|
||||
for f in "$CADDYFILE" "$SITE_FILE"; do
|
||||
if [ -f "$f.before-figcraft" ]; then cp -p "$f.before-figcraft" "$f"; else rm -f "$f"; fi
|
||||
if [[ -f "$f.before-figcraft" ]]; then cp -p "$f.before-figcraft" "$f"; else rm -f "$f"; fi
|
||||
done
|
||||
die "Caddy doesn't accept the new configuration (the old one is back; see: caddy validate --config $CADDYFILE)"
|
||||
fi
|
||||
@@ -356,10 +360,10 @@ fi
|
||||
systemctl enable --now --quiet caddy
|
||||
|
||||
# --- Firewall -------------------------------------------------------------------------------
|
||||
if [ "$FIREWALL" = true ]; then
|
||||
if [[ "$FIREWALL" = true ]]; then
|
||||
# The ports sshd really listens on (sudo doesn't pass SSH_CONNECTION on).
|
||||
SSH_PORTS="$(sshd -T 2>/dev/null | awk '/^port / {print $2}' | sort -u)"
|
||||
[ -n "$SSH_PORTS" ] || SSH_PORTS=22
|
||||
[[ -n "$SSH_PORTS" ]] || SSH_PORTS=22
|
||||
say "Firewall: SSH ($(echo "$SSH_PORTS" | tr '\n' ' ' | sed 's/ $//')), 80 and 443"
|
||||
for p in $SSH_PORTS; do ufw allow "$p/tcp" >/dev/null; done
|
||||
ufw allow 80/tcp >/dev/null
|
||||
@@ -370,12 +374,12 @@ if [ "$FIREWALL" = true ]; then
|
||||
# Other services reachable from outside would be cut off by enabling ufw.
|
||||
OTHERS=""
|
||||
for a in $(ss -Hltn | awk '{print $4}'); do
|
||||
case "$a" in 127.* | "[::1]:"* | ::1:*) continue ;; esac
|
||||
case "$a" in 127.* | "[::1]:"* | ::1:*) continue ;; *) ;; esac
|
||||
port="${a##*:}"
|
||||
case " $(echo "$SSH_PORTS" | tr '\n' ' ') 80 443 " in *" $port "*) continue ;; esac
|
||||
OTHERS="$OTHERS $port"
|
||||
done
|
||||
if [ -n "$OTHERS" ] && [ "$FIREWALL_ANYWAY" = false ]; then
|
||||
if [[ -n "$OTHERS" ]] && [[ "$FIREWALL_ANYWAY" = false ]]; then
|
||||
warn "other services listen on port(s)$OTHERS: ufw was not enabled (rules added; allow those ports, then 'ufw enable', or run again with --firewall-anyway)"
|
||||
else
|
||||
ufw --force enable >/dev/null
|
||||
@@ -395,7 +399,7 @@ cat <<EOF
|
||||
figcraft-server is running behind Caddy.
|
||||
|
||||
Publish: in FigCraft, Share > Publish to server..., address wss://$HOST/
|
||||
$(if [ "$OPEN" = true ]; then echo "(open to the public: no key needed; each file gets an owner token)"; else echo "(with the key: $ADMIN key)"; fi)
|
||||
$(if [[ "$OPEN" = true ]]; then echo "(open to the public: no key needed; each file gets an owner token)"; else echo "(with the key: $ADMIN key)"; fi)
|
||||
Check: curl https://$HOST/healthz (once DNS points here and Caddy has its certificate)
|
||||
Logs: journalctl -u $SERVICE -f
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
arm64) ARCH=aarch64 ;;
|
||||
amd64) ARCH=x86_64 ;;
|
||||
*) ;; # already a release name (x86_64, aarch64)
|
||||
esac
|
||||
NAME="figcraft-server-$VERSION-$OS-$ARCH"
|
||||
echo "==> figcraft-server $VERSION for $OS $ARCH"
|
||||
|
||||
if [ "$SKIP_BUILD" = 0 ]; then
|
||||
if [[ "$SKIP_BUILD" = 0 ]]; then
|
||||
(cd "$ROOT" && cargo build --release --locked -p figcraft-server)
|
||||
fi
|
||||
WORK="$CARGO_TARGET_DIR/server-package"
|
||||
|
||||
@@ -11,13 +11,13 @@ set -euo pipefail
|
||||
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
||||
HERE="$ROOT/packaging/web"
|
||||
|
||||
if [ "${1:-}" != "--skip-build" ]; then
|
||||
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||
command -v trunk >/dev/null || { echo "error: trunk not found (cargo install trunk --locked)" >&2; exit 1; }
|
||||
(cd "$ROOT/apps/figcraft-web" && trunk build --release)
|
||||
fi
|
||||
|
||||
SITE="$ROOT/dist/web"
|
||||
[ -f "$SITE/index.html" ] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
[[ -f "$SITE/index.html" ]] || { echo "error: $SITE/index.html missing; run without --skip-build" >&2; exit 1; }
|
||||
# Paths must be relative so the site works under any prefix (public_url = "./" in Trunk.toml).
|
||||
if grep -Eq '(src|href)="/[^/]' "$SITE/index.html"; then
|
||||
echo "error: $SITE/index.html has root-absolute URLs; it would break when served from a sub-path" >&2
|
||||
@@ -30,16 +30,16 @@ fi
|
||||
MAX_WASM_BYTES="${FIGCRAFT_WASM_MAX_BYTES:-25165824}" # 24 MiB
|
||||
WASM_COUNT=0
|
||||
for wasm in "$SITE"/*.wasm; do
|
||||
[ -f "$wasm" ] || continue
|
||||
[[ -f "$wasm" ]] || continue
|
||||
WASM_COUNT=$((WASM_COUNT + 1))
|
||||
size=$(wc -c <"$wasm" | tr -d ' ')
|
||||
echo "$(basename "$wasm"): $size bytes ($((size / 1048576)) MiB; limit $MAX_WASM_BYTES)"
|
||||
if [ "$size" -gt "$MAX_WASM_BYTES" ]; then
|
||||
if [[ "$size" -gt "$MAX_WASM_BYTES" ]]; then
|
||||
echo "error: $(basename "$wasm") is $size bytes, over the $MAX_WASM_BYTES-byte limit (Cloudflare's per-file cap is 25 MiB)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
[ "$WASM_COUNT" -gt 0 ] || { echo "error: no .wasm in $SITE" >&2; exit 1; }
|
||||
[[ "$WASM_COUNT" -gt 0 ]] || { echo "error: no .wasm in $SITE" >&2; exit 1; }
|
||||
|
||||
NAME="figcraft-web-$VERSION"
|
||||
WORK="$CARGO_TARGET_DIR/web-package"
|
||||
|
||||
@@ -56,8 +56,8 @@ if (-not $haveCert -and -not $haveAzure) {
|
||||
|
||||
$script:SignTool = Find-SignTool
|
||||
$common = @('sign', '/v', '/fd', 'SHA256', '/td', 'SHA256', '/d', 'FigCraft', '/du', 'https://github.com/storytold/figcraft')
|
||||
$tmp = Join-Path ([IO.Path]::GetTempPath()) "figcraft-sign-$PID"
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
$tmp = Join-Path ([IO.Path]::GetTempPath()) "figcraft-sign-$([IO.Path]::GetRandomFileName())"
|
||||
New-Item -ItemType Directory -Path $tmp | Out-Null
|
||||
|
||||
try {
|
||||
if ($haveCert) {
|
||||
@@ -73,8 +73,15 @@ try {
|
||||
Write-Output "Signing with Azure Trusted Signing ($env:AZURE_SIGNING_ACCOUNT / $env:AZURE_CERT_PROFILE): $($Files -join ', ')"
|
||||
# The dlib authenticates with DefaultAzureCredential, which reads AZURE_TENANT_ID,
|
||||
# AZURE_CLIENT_ID and AZURE_CLIENT_SECRET from the environment.
|
||||
$trustedSigningVersion = '1.0.95'
|
||||
$trustedSigningSha256 = '3bfcf1e0a3cb42af1692f0a8ed45c15de070c2de86f28a59b2795d904d8a920f'
|
||||
$pkg = Join-Path $tmp 'trusted-signing.zip'
|
||||
Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.Trusted.Signing.Client' -OutFile $pkg
|
||||
$uri = "https://api.nuget.org/v3-flatcontainer/microsoft.trusted.signing.client/$trustedSigningVersion/microsoft.trusted.signing.client.$trustedSigningVersion.nupkg"
|
||||
Invoke-WebRequest -Uri $uri -OutFile $pkg
|
||||
$actualHash = (Get-FileHash -Path $pkg -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $trustedSigningSha256) {
|
||||
throw "Microsoft.Trusted.Signing.Client $trustedSigningVersion checksum mismatch"
|
||||
}
|
||||
Expand-Archive -Path $pkg -DestinationPath (Join-Path $tmp 'client') -Force
|
||||
$dlib = Get-ChildItem -Path (Join-Path $tmp 'client') -Recurse -Filter Azure.CodeSigning.Dlib.dll |
|
||||
Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1
|
||||
|
||||
+21
-4
@@ -1,6 +1,7 @@
|
||||
//! Project tasks: `cargo xtask <task>`.
|
||||
//!
|
||||
//! - `ci` fmt check, clippy -D warnings, tests, layers, assets, wasm check
|
||||
//! - `test` the workspace tests (nextest when installed) and doctests
|
||||
//! - `layers` crate dependency layering check
|
||||
//! - `assets` every file under assets/ and docs/ has a row in ATTRIBUTION.md
|
||||
//! - `parity` menu-catalog coverage → docs/parity-checklist.md
|
||||
@@ -16,7 +17,7 @@
|
||||
mod icon;
|
||||
mod layers;
|
||||
|
||||
use std::process::{Command, ExitCode};
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
|
||||
fn run(cmd: &mut Command) -> bool {
|
||||
eprintln!("$ {cmd:?}");
|
||||
@@ -36,6 +37,7 @@ fn main() -> ExitCode {
|
||||
let task = args.first().map(String::as_str).unwrap_or("help");
|
||||
let ok = match task {
|
||||
"ci" => ci(),
|
||||
"test" => test(),
|
||||
"layers" => layers::check(&root()),
|
||||
"assets" => assets(),
|
||||
"parity" => parity(),
|
||||
@@ -45,7 +47,7 @@ fn main() -> ExitCode {
|
||||
"plugins" => plugins(),
|
||||
"bench" => bench(args.get(1).map_or("1000,10000,50000", String::as_str)),
|
||||
_ => {
|
||||
eprintln!("tasks: ci, layers, assets, parity, version [set X.Y.Z], icon, wasm, plugins, bench [SIZES]");
|
||||
eprintln!("tasks: ci, test, layers, assets, parity, version [set X.Y.Z], icon, wasm, plugins, bench [SIZES]");
|
||||
task == "help"
|
||||
}
|
||||
};
|
||||
@@ -54,16 +56,31 @@ fn main() -> ExitCode {
|
||||
|
||||
fn ci() -> bool {
|
||||
run(cargo().args(["fmt", "--all", "--check"]))
|
||||
&& run(cargo().args(["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]))
|
||||
&& run(cargo().args(["test", "--workspace"]))
|
||||
&& run(cargo().args(["clippy", "--workspace", "--all-targets", "--keep-going", "--", "-D", "warnings"]))
|
||||
&& test()
|
||||
&& layers::check(&root())
|
||||
&& assets()
|
||||
&& wasm()
|
||||
}
|
||||
|
||||
/// Unit and integration tests with nextest when it is installed (`cargo install cargo-nextest`),
|
||||
/// else with `cargo test`; then the doctests, which nextest does not run. Examples are not built:
|
||||
/// clippy's `--all-targets` already checks them.
|
||||
fn test() -> bool {
|
||||
let nextest = cargo().args(["nextest", "--version"]).stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success());
|
||||
let units = if nextest {
|
||||
run(cargo().args(["nextest", "run", "--workspace", "--lib", "--bins", "--tests"]))
|
||||
} else {
|
||||
eprintln!("xtask: cargo-nextest not installed; running unit and integration tests with `cargo test`");
|
||||
run(cargo().args(["test", "--workspace", "--lib", "--bins", "--tests"]))
|
||||
};
|
||||
units && run(cargo().args(["test", "--workspace", "--doc"]))
|
||||
}
|
||||
|
||||
fn wasm() -> bool {
|
||||
run(cargo().args([
|
||||
"check",
|
||||
"--keep-going",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
"-p",
|
||||
|
||||
Reference in New Issue
Block a user