ci: the Flatpak workflow, local deps for the local build, the lens tools

- .forgejo/workflows/ci.yaml is Crafter.Build's workflow for this app: the
  base installed from the Catcrafts repo, the version stamped into the
  crafter-review module, the suite inside the build, then a smoke test of
  the app as it ships - the version and home, every library resolved and no
  RUNPATH outside /app, the bundled tools, a freeze and a dry run on a
  scratch repository. Publishing holds /srv/flatpak/.publish.lock and
  writes only this app's flatpakref. Unverified until a crafter-flatpak
  runner serves this repository.
- `run flatpak --deps=local` builds against the Crafter.Network and
  Crafter.Thread checkouts next to this one, as a native --local build
  does, while their commits are not on forgejo: a gitignored manifest copy
  with three dir sources side by side and --local.
- LensTools: every program the lens allowlist names, the shell and the MCP
  server's node and npx resolve on PATH; strict in the Flatpak build, where
  the suite runs in the sandbox that ships, a skip natively when this
  machine lacks one.

Built on the stable base (Crafter.Build 3.0.0) with --deps=local: the suite
passes inside flatpak-builder (25 passed, 2 skipped), and the workflow's
smoke test passes against that build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 15:59:20 +01:00
commit 5dab4b49d5
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
3 changed files with 373 additions and 17 deletions

274
.forgejo/workflows/ci.yaml Normal file
View file

@ -0,0 +1,274 @@
# SPDX-License-Identifier: AGPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
name: Flatpak
# Crafter.Build's workflow, for this app. One job, three outcomes:
# pull request -> build the Flatpak, run the suite inside it, smoke-test it
# push master -> also publish it as the `beta` branch of the Catcrafts
# repo and refresh the rolling `latest` release
# tag X.Y.Z -> publish it as `stable` and create release X.Y.Z
#
# It builds on the Crafter.Build Flatpak (`base:` in the manifest), whose
# crafter-build and LLVM compile this project, so the runner installs that
# base from the same repo it publishes into.
#
# The `crafter-flatpak` label is the privileged runner on the Hetzner box
# (bubblewrap needs it) with the Sdk install, the flatpak-builder state, the
# repo's web root and the signing key mounted. It is registered to
# Crafter.Build alone today: until this repository has such a runner, this
# workflow does not run.
#
# Both apps publish into one repo: the publish step holds
# /srv/flatpak/.publish.lock, which protects anything once Crafter.Build's
# workflow takes it too. This one writes its own flatpakref and never the
# shared catcrafts.flatpakrepo, which Crafter.Build's workflow owns.
on:
pull_request:
branches: [master]
push:
branches: [master]
tags: ['[0-9]*.[0-9]*.[0-9]*']
workflow_dispatch:
env:
APP_ID: net.catcrafts.CrafterReview
MANIFEST: packaging/flatpak/net.catcrafts.CrafterReview.yaml
CI_MANIFEST: packaging/flatpak/ci-manifest.yaml
METAINFO: packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml
RUNTIME: org.freedesktop.Sdk//26.08
BASE: net.catcrafts.CrafterBuild//stable
REPO_DIR: /srv/flatpak/repo
WEB_DIR: /srv/flatpak
jobs:
flatpak:
runs-on: crafter-flatpak
container:
image: archlinux:latest
options: >-
-v crafter-flatpak-system:/var/lib/flatpak
-v crafter-flatpak-builder:/root/.cache/flatpak-builder
-v /var/www/flatpak:/srv/flatpak
-v /etc/crafter-flatpak:/run/crafter-flatpak:ro
steps:
- name: Install tools
run: |
pacman-key --init
pacman-key --populate archlinux
pacman -Sy --noconfirm --needed archlinux-keyring
pacman -Syu --noconfirm --needed flatpak flatpak-builder ccache git jq gnupg go-yq ostree python util-linux
git config --global --add safe.directory '*'
- name: Sdk and base app
# A no-op once the volume is warm; `update` follows point releases
# and a new stable base.
run: |
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo
flatpak install -y --noninteractive flathub "$RUNTIME"
flatpak install -y --noninteractive catcrafts "$BASE"
flatpak update -y --noninteractive
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Version and channel
id: meta
run: |
set -eu
if [ "${GITHUB_REF_TYPE}" = tag ]; then
version="$GITHUB_REF_NAME"
branch=stable
# The metainfo's newest release is the release notes and must be
# the version being tagged.
top=$(grep -m1 -oP '<release version="\K[^"]+' "$METAINFO")
if [ "$top" != "$version" ]; then
echo "metainfo lists $top as the newest release, but the tag is $version" >&2
exit 1
fi
else
# Named after the release it follows, as a checkout describes
# itself: 1.0.0-3-g21746b1 is three commits past 1.0.0.
described=$(git describe --tags --always --exclude=latest)
case "$described" in
[0-9]*.[0-9]*.[0-9]*) version="$described" ;;
*) version="0.0.0-dev+$described" ;;
esac
branch=beta
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
echo "version $version, flatpak branch $branch"
- name: Build
# The manifest is rewritten beside the original so its relative
# source paths still resolve. The suite runs inside this build.
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -eu
yq '(.modules[] | select(.name == "crafter-review") | ."build-options".env.CRAFTER_REVIEW_VERSION) = strenv(VERSION)' "$MANIFEST" > "$CI_MANIFEST"
grep -n 'CRAFTER_REVIEW_VERSION' "$CI_MANIFEST"
flatpak-builder --force-clean --ccache --disable-rofiles-fuse \
--state-dir=/root/.cache/flatpak-builder \
--repo=repo --default-branch="${{ steps.meta.outputs.branch }}" \
build-dir "$CI_MANIFEST"
- name: Smoke test
# In the app as it ships: the version and the home, every library
# resolved and none through a RUNPATH outside /app, the bundled
# tools, and a freeze and a dry run on a scratch repository (a dry run
# joins no board and takes no lifetime lock, so it runs inside
# `flatpak build`; it exits 2 by design).
env:
VERSION: ${{ steps.meta.outputs.version }}
run: |
set -eu
shown=$(flatpak build build-dir crafter-review --version)
printf '%s\n' "$shown"
printf '%s\n' "$shown" | grep -qF "crafter-review $VERSION (prompt v"
flatpak build build-dir sh -euc '
for f in /app/bin/crafter-review /app/lib/node/bin/node; do
if ldd "$f" | grep "not found"; then echo "$f: a library is missing" >&2; exit 1; fi
bad=$(readelf -d "$f" | sed -n "s/.*(R[UN]*PATH).*\[\(.*\)\]/\1/p" | tr ":" "\n" | grep -v -e "^\$" -e "^/app" -e "^\\\$ORIGIN" || true)
if [ -n "$bad" ]; then echo "$f: a RUNPATH outside /app: $bad" >&2; exit 1; fi
done
rg --version | head -1
node --version
npx --version
git --version'
flatpak build build-dir sh -euc '
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
export GIT_AUTHOR_NAME=CI GIT_AUTHOR_EMAIL=ci@catcrafts.net GIT_COMMITTER_NAME=CI GIT_COMMITTER_EMAIL=ci@catcrafts.net
cd "$(mktemp -d)"
git init -q -b main .
git config user.email ci@catcrafts.net
printf ".review-gates/push/\n.review-gates/trees/\n" > .gitignore
mkdir .review-gates
printf "{\"lenses\": {\"repo-functional\": {\"run\": false}}}\n" > .review-gates/config.json
git add -A
git commit -q -m base
base=$(git rev-parse HEAD)
printf "def hello(name):\n return name\n" > hello.py
git add hello.py
git commit -q -m "add hello"
crafter-review freeze --slug ci --base "$base"
hash=$(jq -r .hash .review-gates/push/ci/freeze.json)
code=0
crafter-review run --artifact .review-gates/push/ci --hash "$hash" --base "$base" --trees . \
--dispositions .review-gates/push/ci/gate-state/dispositions.json --scratch "$PWD/scratch" --dry-run || code=$?
if [ "$code" != 2 ]; then echo "the dry run exited $code, not 2" >&2; exit 1; fi
ls scratch/*-dry/prompts | head -3'
# Everything below publishes and only runs on master and on tags.
- name: Bundle
if: github.event_name != 'pull_request'
run: |
mkdir -p dist
flatpak build-bundle repo "dist/crafter-review-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "${{ steps.meta.outputs.branch }}"
cp packaging/flatpak/net.catcrafts.CrafterReview.flatpakref dist/
(cd dist && sha256sum * > SHA256SUMS)
ls -la dist
- name: Upload bundle
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v3
with:
name: crafter-review-${{ steps.meta.outputs.version }}
path: dist/*.flatpak
if-no-files-found: error
- name: Publish to the Catcrafts repo
if: github.event_name != 'pull_request'
run: |
set -eu
if [ ! -s /run/crafter-flatpak/signing-key.asc ] || [ ! -s /run/crafter-flatpak/key-id ]; then
echo "no signing key under /etc/crafter-flatpak on the runner host (Crafter.Build's AGENTS.md, CI and publishing)" >&2
exit 1
fi
export GNUPGHOME=$(mktemp -d)
gpg --batch --quiet --import /run/crafter-flatpak/signing-key.asc
FLATPAK_GPG_KEY_ID=$(cat /run/crafter-flatpak/key-id)
# One publish at a time into the repo both apps share: a commit and
# the summary update of one workflow never interleave with the
# other's.
exec 9> "$WEB_DIR/.publish.lock"
flock 9
[ -f "$REPO_DIR/config" ] || ostree init --repo="$REPO_DIR" --mode=archive
flatpak build-commit-from --src-repo=repo \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
"$REPO_DIR" "app/$APP_ID/x86_64/${{ steps.meta.outputs.branch }}"
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
--generate-static-deltas --prune "$REPO_DIR"
# This app's ref file only: catcrafts.flatpakrepo is Crafter.Build's.
key=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 --wrap=0)
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/net.catcrafts.CrafterReview.flatpakref > "$WEB_DIR/net.catcrafts.CrafterReview.flatpakref"
flock -u 9
- name: Rolling 'latest' release
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
run: |
git config user.email "ci@catcrafts.net"
git config user.name "Crafter Review CI"
git tag -f latest
git push origin latest --force
- name: Publish rolling 'latest' release
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
url: ${{ github.server_url }}
repo: ${{ github.repository }}
tag: latest
title: Latest master build
prerelease: true
override: true
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}
- name: Release notes
if: github.ref_type == 'tag'
run: |
# The newest <release> block's description as Markdown, the way
# Crafter.Build's workflow writes its notes.
python3 - > release-notes.md <<'EOF'
import xml.etree.ElementTree as ET
desc = ET.parse("packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml").getroot().find("releases/release/description")
def text(el):
parts = [el.text or ""]
for child in el:
inner = text(child)
parts.append({"em": f"*{inner}*", "code": f"`{inner}`"}.get(child.tag, inner))
parts.append(child.tail or "")
return " ".join("".join(parts).split())
blocks = list(desc)
out = []
for i, el in enumerate(blocks):
heads_list = i + 1 < len(blocks) and blocks[i + 1].tag in ("ul", "ol")
if el.tag == "p":
out.append(f"## {text(el)}" if heads_list else text(el))
elif el.tag == "ul":
out.append("\n".join(f"- {text(li)}" for li in el))
elif el.tag == "ol":
out.append("\n".join(f"{n}. {text(li)}" for n, li in enumerate(el, 1)))
print("\n\n".join(out))
print("\nInstall:\n\n flatpak remote-add --if-not-exists catcrafts https://flatpak.catcrafts.net/catcrafts.flatpakrepo\n flatpak install catcrafts net.catcrafts.CrafterReview\n")
EOF
- name: Publish release
if: github.ref_type == 'tag'
uses: https://code.forgejo.org/actions/forgejo-release@v2
with:
direction: upload
url: ${{ github.server_url }}
repo: ${{ github.repository }}
tag: ${{ github.ref_name }}
title: crafter-review ${{ github.ref_name }}
release-notes-file: release-notes.md
release-dir: dist
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -84,7 +84,7 @@ namespace {
// One per directory under tests/, each an ordinary main() that links the // One per directory under tests/, each an ordinary main() that links the
// library: exit 0 passes, nonzero fails, 77 skips. // library: exit 0 passes, nonzero fails, 77 skips.
constexpr std::array<std::string_view, 25> Tests = { constexpr std::array<std::string_view, 26> Tests = {
"Crypto", "Crypto",
"Text", "Text",
"Platform", "Platform",
@ -110,6 +110,7 @@ namespace {
"ReviewFixes", "ReviewFixes",
"Runs", "Runs",
"Commands", "Commands",
"LensTools",
}; };
// The version the binary reports: CI sets it from the tag, a checkout // The version the binary reports: CI sets it from the tag, a checkout
@ -129,7 +130,9 @@ namespace {
// Crafter.Build's own scripts do. --base=<branch> builds on another branch // Crafter.Build's own scripts do. --base=<branch> builds on another branch
// of the Crafter.Build base app than stable: until a Crafter.Build release // of the Crafter.Build base app than stable: until a Crafter.Build release
// with scripts is on stable, `--base=local` (the branch Crafter.Build's own // with scripts is on stable, `--base=local` (the branch Crafter.Build's own
// `run install` makes) is what builds this project. // `run install` makes) is what builds this project. --deps=local takes
// Crafter.Network and Crafter.Thread from the checkouts next to this one,
// as a native --local build does, for commits not on forgejo yet.
struct FlatpakPaths { struct FlatpakPaths {
fs::path manifest; fs::path manifest;
// flatpak-builder's downloads, ccache and per-module cache. // flatpak-builder's downloads, ccache and per-module cache.
@ -142,14 +145,15 @@ namespace {
// Everything under <project>/build/flatpak, a fixed name rather than a // Everything under <project>/build/flatpak, a fixed name rather than a
// BuildDir(): the Flatpak is compiled by the toolchain inside the sandbox, // BuildDir(): the Flatpak is compiled by the toolchain inside the sandbox,
// not by the host variant. flatpak-builder keys its cache on the // not by the host variant. flatpak-builder keys its cache on the
// manifest's absolute path, so a branch other than stable gets a manifest // manifest's absolute path, so a branch other than stable, or local deps,
// copy of its own next to the original (gitignored), and with it a cache // get a manifest copy of their own next to the original (gitignored), and
// of its own. // with it a cache of their own.
FlatpakPaths FlatpakLayout(const fs::path& projectDir, std::string_view base) { FlatpakPaths FlatpakLayout(const fs::path& projectDir, std::string_view base, bool localDeps = false) {
const fs::path root = projectDir / "build" / "flatpak"; const fs::path root = projectDir / "build" / "flatpak";
const fs::path packaging = projectDir / "packaging" / "flatpak"; const fs::path packaging = projectDir / "packaging" / "flatpak";
const bool original = base == "stable" && !localDeps;
return { return {
.manifest = base == "stable" ? packaging / "net.catcrafts.CrafterReview.yaml" : packaging / std::format("{}-base.yaml", base), .manifest = original ? packaging / "net.catcrafts.CrafterReview.yaml" : packaging / std::format("{}{}-base.yaml", base, localDeps ? "-localdeps" : ""),
.state = root / "state", .state = root / "state",
.repo = root / "repo", .repo = root / "repo",
.buildDir = root / "build-dir", .buildDir = root / "build-dir",
@ -187,18 +191,52 @@ namespace {
return url; return url;
} }
// `old` replaced by `replacement` once in `text`; false when it is not
// there.
bool ReplaceOnce(std::string& text, std::string_view old, std::string_view replacement) {
const std::size_t at = text.find(old);
if (at == std::string::npos) return false;
text.replace(at, old.size(), replacement);
return true;
}
// The crafter-review module built from three dir sources side by side,
// so --local finds ../Crafter.Network and ../Crafter.Thread.
bool WithLocalDeps(std::string& manifest) {
constexpr std::string_view Source = R"( - type: dir
path: ../..
skip:
- .git
- bin
- build
)";
std::string sources;
for (const std::string_view project : {"Crafter.Review", "Crafter.Network", "Crafter.Thread"}) {
const std::string path = project == "Crafter.Review" ? std::string("../..") : std::format("../../../{}", project);
sources += std::format(" - type: dir\n path: {}\n dest: {}\n skip:\n - .git\n - bin\n - build\n", path, project);
}
return ReplaceOnce(manifest, " - name: crafter-review\n", " - name: crafter-review\n subdir: Crafter.Review\n")
&& ReplaceOnce(manifest, " - crafter-build\n - crafter-build test\n", " - crafter-build --local\n - crafter-build --local test\n")
&& ReplaceOnce(manifest, Source, sources);
}
std::int32_t BuildFlatpak(ScriptContext& ctx) { std::int32_t BuildFlatpak(ScriptContext& ctx) {
if (RefusedInSandbox("flatpak")) return 1; if (RefusedInSandbox("flatpak")) return 1;
std::string jobs; std::string jobs;
std::string base = "stable"; std::string base = "stable";
bool localDeps = false;
for (const std::string& arg : ctx.args) { for (const std::string& arg : ctx.args) {
if (arg.starts_with("--base=") && arg.size() > 7) { if (arg.starts_with("--base=") && arg.size() > 7) {
base = arg.substr(7); base = arg.substr(7);
continue; continue;
} }
if (arg == "--deps=local") {
localDeps = true;
continue;
}
const std::string_view count = arg.starts_with("--jobs=") ? std::string_view(arg).substr(7) : std::string_view(); const std::string_view count = arg.starts_with("--jobs=") ? std::string_view(arg).substr(7) : std::string_view();
if (count.empty() || !std::ranges::all_of(count, [](char c) { return c >= '0' && c <= '9'; })) { if (count.empty() || !std::ranges::all_of(count, [](char c) { return c >= '0' && c <= '9'; })) {
std::println(std::cerr, "run flatpak: unknown argument '{}' (only --jobs=<N> and --base=<branch> are accepted)", arg); std::println(std::cerr, "run flatpak: unknown argument '{}' (only --jobs=<N>, --base=<branch> and --deps=local are accepted)", arg);
return 1; return 1;
} }
jobs = std::format(" --jobs={}", count); jobs = std::format(" --jobs={}", count);
@ -216,18 +254,20 @@ namespace {
std::println(std::cerr, "run flatpak: neither flatpak-builder nor the org.flatpak.Builder app is installed (flatpak install flathub org.flatpak.Builder)"); std::println(std::cerr, "run flatpak: neither flatpak-builder nor the org.flatpak.Builder app is installed (flatpak install flathub org.flatpak.Builder)");
return 1; return 1;
} }
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir, base); const FlatpakPaths paths = FlatpakLayout(ctx.projectDir, base, localDeps);
if (base != "stable") { if (base != "stable" || localDeps) {
// The manifest with only base-version changed. // The manifest with base-version changed, and the local deps.
std::ifstream in(FlatpakLayout(ctx.projectDir, "stable").manifest); std::ifstream in(FlatpakLayout(ctx.projectDir, "stable").manifest);
const std::string manifest((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); std::string manifest((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
constexpr std::string_view Line = "base-version: stable\n"; if (!ReplaceOnce(manifest, "base-version: stable\n", std::format("base-version: {}\n", base))) {
const std::size_t at = manifest.find(Line); std::println(std::cerr, "run flatpak: the manifest has no `base-version: stable` line to replace");
if (at == std::string::npos) {
std::println(std::cerr, "run flatpak: the manifest has no `{}` line to replace", Line.substr(0, Line.size() - 1));
return 1; return 1;
} }
std::ofstream(paths.manifest) << manifest.substr(0, at) << std::format("base-version: {}\n", base) << manifest.substr(at + Line.size()); if (localDeps && !WithLocalDeps(manifest)) {
std::println(std::cerr, "run flatpak: the crafter-review module is not in the shape --deps=local rewrites (its dir source and its two crafter-build lines)");
return 1;
}
std::ofstream(paths.manifest) << manifest;
} }
const std::string buildDir = ShellQuote(paths.buildDir.string()); const std::string buildDir = ShellQuote(paths.buildDir.string());
if (const std::int32_t code = ctx.Run(std::format("{}{} --user --force-clean --ccache --state-dir={} --repo={} --default-branch=local {} {}", builder, jobs, ShellQuote(paths.state.string()), ShellQuote(paths.repo.string()), buildDir, ShellQuote(paths.manifest.string()))); code != 0) return code; if (const std::int32_t code = ctx.Run(std::format("{}{} --user --force-clean --ccache --state-dir={} --repo={} --default-branch=local {} {}", builder, jobs, ShellQuote(paths.state.string()), ShellQuote(paths.repo.string()), buildDir, ShellQuote(paths.manifest.string()))); code != 0) return code;

42
tests/LensTools/main.cpp Normal file
View file

@ -0,0 +1,42 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The programs a lens is allowed to run are there: every Bash(<program>
// ...:*) entry of the base allowlist (the git verbs included), the shell
// claude's Bash tool runs them with, and node and npx for the Playwright MCP
// server. The suite runs inside the Flatpak build, the sandbox that ships, so
// there every one must resolve on PATH; natively the test names what this
// machine lacks and skips.
#include <unistd.h>
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
int main() {
std::println("lens tools: every program a lens's allowlist names is on PATH");
std::set<std::string> programs = {"bash", "node", "npx"};
for (const std::string& tool : Tools::BaseAllow()) {
if (!tool.starts_with("Bash(") || !tool.ends_with(":*)")) continue;
const std::string prefix = tool.substr(5, tool.size() - 8);
const std::string program = prefix.substr(0, prefix.find(' '));
// A shell builtin, never a file of its own.
if (program != "cd") programs.insert(program);
}
const std::vector<std::string> path = Dyn::Split(Platform::GetEnv("PATH").value_or(""), ":");
std::vector<std::string> missing;
for (const std::string& program : programs) {
const bool found = std::ranges::any_of(path, [&](const std::string& dir) { return !dir.empty() && access((fs::path(dir) / program).c_str(), X_OK) == 0; });
if (!found) missing.push_back(program);
}
const bool shipped = fs::exists("/.flatpak-info");
if (!missing.empty() && !shipped) {
std::println("skipped: not on this machine's PATH: {}", Dyn::Join(missing, ", "));
return 77;
}
const bool ok = missing.empty() && programs.size() >= 20;
std::println("{}every program a lens's allowlist names, its shell and the MCP server's node and npx resolve on PATH ({} programs){}", ok ? " ok " : " FAIL ", programs.size(),
missing.empty() ? std::string() : std::format(" (missing: {})", Dyn::Join(missing, ", ")));
return ok ? 0 : 1;
}