Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,21 @@ jobs:
sudo iptables -P FORWARD ACCEPT
sudo snap install snapcraft --classic

# snapcraft.yaml builds the app from source inside LXD, so this job needs
# the checkout above and nothing from the release.
# The snap is a repackaging of this release's own .deb, so what the Snap
# Store serves is the binary the release page serves rather than a second
# compilation of the same source. snapcraft.yaml reads it from here.
- name: Take the .deb from the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${TAG#v}"
gh release download "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern "Markpad_${VERSION}_amd64.deb" \
--output markpad.deb
dpkg-deb -f markpad.deb Package Version Architecture

- name: Build and publish the snap
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
39 changes: 38 additions & 1 deletion .github/workflows/test_snap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,45 @@ jobs:
# one line away rather than a bisect.
snap list snapcraft

# snapcraft.yaml packages a .deb rather than building one, and a pull
# request has no release of its own to take one from. The latest release's
# is the right stand-in: what is under test here is snapcraft.yaml, and a
# .deb is a .deb. The snap this produces is thrown away.
- name: Take a .deb to package
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release download \
--repo "$GITHUB_REPOSITORY" \
--pattern '*_amd64.deb' \
--output markpad.deb
dpkg-deb -f markpad.deb Package Version Architecture

- name: Pack
run: |
set -euo pipefail
sg lxd -c 'snapcraft pack'
sg lxd -c 'snapcraft pack' 2>&1 | tee pack.log
ls -la ./*.snap

# During priming, snapcraft resolves what the staged binaries need against
# the base, the extension and stage-packages, and names anything it could
# not place -- as a warning, and a warning in a several-hundred-line log
# is not a result. This is the one place the resolution happens in the
# environment the snap will actually run in, so it is worth promoting to a
# failure. The binary names 16 libraries; two of them, libssl.so.3 and
# libcrypto.so.3, are in neither stage-packages nor obviously in the gnome
# runtime, which is what makes this worth asserting rather than assuming.
#
# It reads snapcraft's wording, so a reworded warning would pass silently.
# Running the snap is what would catch that, and needs a display; the
# AppImage smoke test is where that belongs.
- name: snapcraft placed every library the binary asks for
run: |
set -euo pipefail
if grep -qi 'missing dependencies' pack.log; then
grep -i -A20 'missing dependencies' pack.log
echo "::error::snapcraft could not place a library the binary needs" >&2
exit 1
fi
echo "No missing-dependency report in the pack log."
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ src-tauri/output.txt
dist/
.cache/
.worktrees/

# Downloaded by the snap jobs; snapcraft.yaml packages it. Never committed.
markpad.deb
pack.log
47 changes: 24 additions & 23 deletions scripts/releaseWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,30 @@ test('release builds install the locked dependency graph', () => {
assert.doesNotMatch(workflow, /npm install/);
});

test('the snap build installs the locked dependency graph too', () => {
// RELEASING.md promises every release channel resolves exactly the committed
// lockfile. The snap is built by snapcraft, not by build.yml, so the
// assertion above cannot see it.
assert.match(snapcraft, /^\s+npm ci$/m);
assert.doesNotMatch(snapcraft, /npm install/);
test('the snap ships the binary the release shipped, not a second build of it', () => {
// This used to assert `npm ci` in snapcraft.yaml, because the snap resolved
// its own dependency graph and compiled its own binary. RELEASING.md promises
// every channel resolves the committed lockfile, and two compilations of one
// source satisfy that while still producing two different binaries: a snap
// user and a .deb user of the same version did not have the same file.
//
// Packaging the release's .deb makes the promise structural instead. It also
// takes rust and node out of a file that had no other reason to hold them.
assert.match(snapcraft, /^\s*source-type: deb$/m);
for (const builder of [/npm ci/, /npm install/, /tauri build/, /rustup/, /cargo/]) {
assert.doesNotMatch(snapcraft, builder, `snapcraft.yaml is building, not packaging: ${builder}`);
}
});

test('the .deb the snap packages is the one from the release being published', () => {
// The whole guarantee is that the Snap Store serves what the release page
// serves. It rests on where this file comes from, so the download is pinned
// to the tag and to the exact name build.yml uploads — not `latest`, and not
// a pattern that could match a different architecture.
const snapJob = sliceFrom(publishWorkflow, '\n snap:');
assert.match(snapJob, /gh release download "\$TAG"/);
assert.match(snapJob, /--pattern "Markpad_\$\{VERSION\}_amd64\.deb"/);
assert.match(snapJob, /--output markpad\.deb/);
});

test('the runtime version and the frontend version cannot drift apart', () => {
Expand Down Expand Up @@ -159,23 +177,6 @@ test('a publishing step cannot report success while failing', () => {
assert.doesNotMatch(workflow, /continue-on-error/);
});

test('the snap build can find rustup', () => {
// Rust has to be installed by a phase that runs before any part is pulled,
// because the alternative -- a part that installs it -- is too late for the
// part that pulls alongside it. `after:` orders build and stage, not pull.
// build-packages is that phase, and apt is also the only carrier tried that
// is an ordinary ELF: the rustup snap is linked against its own runtime and
// picked the gnome SDK's libc.so.6 off LD_LIBRARY_PATH.
assert.match(snapcraft, /^\s*build-packages:\n(?:\s*-\s.*\n)*\s*-\s*rustup$/m);
// And nothing may ask snapcraft's rust plugin to validate the environment.
// It reported `'rustup' not found` twice with rustup present and runnable,
// once from the snap and once from apt. snapcraft.yaml has the run IDs.
assert.doesNotMatch(snapcraft, /^\s*plugin: rust$/m);
// apt's cargo and rustc are shims that refuse to run until a toolchain is
// chosen, and with no rust plugin there is no pull script choosing one.
assert.match(snapcraft, /^\s*rustup default stable$/m);
});

test('the AppImage strip is a script, and its excludelist has one home', () => {
// This step failed in two of the three v2.7.2 release attempts. As a heredoc
// inside build.yml it could only be exercised by cutting a release; as a
Expand Down
117 changes: 28 additions & 89 deletions snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ grade: stable

apps:
markpad:
command: bin/markpad
desktop: usr/share/applications/markpad.desktop
# Both paths are the .deb's own layout, not this file's invention.
command: usr/bin/Markpad
desktop: usr/share/applications/Markpad.desktop
extensions: [gnome]
plugs:
- network
Expand All @@ -45,99 +46,37 @@ apps:
- home

parts:
# `plugin: nil` with rust from apt, after three runs of test_snap.yml spent
# trying to satisfy `plugin: rust` instead. All three are recorded here
# because each one looks like the obvious thing to try next.
# The snap packages the .deb the release already shipped. It does not build
# Markpad -- build.yml built it once on ubuntu-24.04, and core24 is Ubuntu
# 24.04, so there is nothing a second compilation inside LXD would produce
# that the first did not.
#
# `build-snaps: [rustup/latest/stable]`, which is what v2.7.3 shipped.
# `extensions: [gnome]` puts the gnome SDK's libraries on LD_LIBRARY_PATH
# for every part, and a classic snap's binary is linked against its own
# runtime, so it resolved libc.so.6 out of the SDK and died on a symbol
# only ever resolved between a matched libc.so.6 and ld.so --
# It used to do exactly that, and it cost more than the eight minutes. The
# snap carried a *different binary* from the .deb and the AppImage of the
# same version: one source, two independent compilations, two artifacts. And
# compiling meant rust and node in here, which is what four release-blocking
# failures were about -- #566, then runs 31479631082, 31480354438 and
# 31481081985 on the pull request that fixed it. None of that has anywhere
# left to go wrong, because none of it is here.
#
# /snap/rustup/1504/bin/rustup: symbol lookup error:
# /snap/gnome-46-2404-sdk/current/usr/lib/x86_64-linux-gnu/libc.so.6:
# undefined symbol: __nptl_change_stack_perm, version GLIBC_PRIVATE
#
# -- the same shape as #463 and #499 on the AppImage side, a packaging
# layer putting its own copy of a library ahead of the one that had to be
# used, moved from the user's machine to the builder. Prefixing the call
# with `env -u LD_LIBRARY_PATH` fixes the one this file makes and not the
# ones snapcraft makes (run 31479631082).
#
# The rustup.rs installer in a `rust-deps` part. `after:` orders build and
# stage, not pull: both parts pull in the same second, so the rust plugin's
# pull script runs `rustup update stable` before any override-build has
# executed (run 31480354438) --
#
# Pulling rust-deps / Pulling markpad both 10:06:42
# /root/parts/markpad/run/pull.sh: line 4: rustup: command not found
#
# apt's rustup, which does install /usr/bin/rustup, /usr/bin/cargo and
# /usr/bin/rustc. The plugin reported `'rustup' not found` anyway
# (run 31481081985), as it had in run 31479631082 with the snap's rustup
# on PATH and demonstrably runnable.
#
# Twice the binary was there and the plugin said it was not, so the plugin's
# environment validation is the thing to stop asking to pass. Nothing here
# uses the plugin: override-build below runs npm and tauri and installs the
# binary itself, and has since the snap was first added. `plugin: rust`
# contributed a build step this file overrides, a pull script that runs
# rustup, and the validation. `plugin: nil` keeps `source:` and drops all
# three; rust comes from build-packages, which are installed before any part
# is pulled.
#
# None of this was reachable before this file was built on a pull request.
# v2.6.11 was the last version to reach the Snap Store; the release workflow
# swallowed every failure since under `continue-on-error` and reported green
# for three months and six versions while the store told those users they
# were current.
# `markpad.deb` is put next to this file by the job that runs snapcraft:
# .github/workflows/publish-packages.yml takes it from the release being
# published, and .github/workflows/test_snap.yml from the latest release.
# Missing, snapcraft fails at pull with the path in the message.
markpad:
plugin: nil
source: .
build-packages:
- rustup
- libwebkit2gtk-4.1-dev
- build-essential
- curl
- wget
- file
- libssl-dev
- libgtk-3-dev
- libappindicator3-dev
- librsvg2-dev
plugin: dump
source: markpad.deb
source-type: deb
# The 16 libraries the binary asks for by name: core24 supplies libc, libm
# and libgcc_s; the gnome extension supplies the GTK and glib stack; these
# three are the rest.
stage-packages:
- libwebkit2gtk-4.1-0
- libsoup-3.0-0
- libjavascriptcoregtk-4.1-0
build-snaps:
- node/20/stable
override-build: |
set -e
# apt's /usr/bin/cargo and /usr/bin/rustc are rustup shims and refuse to
# run until a toolchain is chosen. Nothing else does this now: with
# `plugin: nil` there is no pull script to run it first.
rustup default stable
craftctl set version="$(node -p "require('./package.json').version")"
npm ci
export CARGO_BUILD_JOBS=2
npx tauri build --no-bundle

# The binary is built in src-tauri/target/release/
install -D -m755 src-tauri/target/release/Markpad $CRAFT_PART_INSTALL/bin/markpad

# Install the icon
install -D -m644 src-tauri/icons/128x128.png $CRAFT_PART_INSTALL/usr/share/icons/hicolor/128x128/apps/markpad.png
install -D -m644 src-tauri/icons/32x32.png $CRAFT_PART_INSTALL/usr/share/icons/hicolor/32x32/apps/markpad.png

# Install the desktop file
cat << EOF > markpad.desktop
[Desktop Entry]
Name=Markpad
Exec=markpad
Icon=markpad
Type=Application
Categories=Utility;TextEditor;
MimeType=text/markdown;text/plain;
EOF
install -D -m644 markpad.desktop $CRAFT_PART_INSTALL/usr/share/applications/markpad.desktop
# From the artifact rather than from package.json, so the snap cannot be
# labelled a version it does not contain. dpkg-deb is part of dpkg.
craftctl set version="$(dpkg-deb -f "$CRAFT_PROJECT_DIR/markpad.deb" Version)"
craftctl default