Skip to content

Commit 500ac8e

Browse files
Add strict-confinement snap with native messaging for host and snap browsers.
1 parent 7551592 commit 500ac8e

9 files changed

Lines changed: 2104 additions & 611 deletions

File tree

electron/flatpak-paths.cjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ function isFlatpakRuntime(options = {}) {
99
return Boolean(env.FLATPAK_ID) || existsSync(flatpakInfoPath)
1010
}
1111

12+
function isSnapRuntime(options = {}) {
13+
const env = options.env || process.env
14+
return Boolean(env.SNAP_NAME)
15+
}
16+
17+
function getSnapRealHome(options = {}) {
18+
const env = options.env || process.env
19+
// Inside snap, $HOME is remapped to ~/snap/<name>/<rev>. The user's real
20+
// home (where host-side browsers read NativeMessagingHosts) is exposed as
21+
// SNAP_REAL_HOME by snapd. Fall back to HOME outside the sandbox.
22+
if (!isSnapRuntime(options)) return env.HOME || ''
23+
return env.SNAP_REAL_HOME || env.HOME || ''
24+
}
25+
1226
function getHostHome(options = {}) {
1327
const env = options.env || process.env
1428
if (!isFlatpakRuntime(options)) return env.HOME || ''
@@ -64,6 +78,8 @@ module.exports = {
6478
getFlatpakCompatRoots,
6579
getHostHome,
6680
getSandboxSafePath,
81+
getSnapRealHome,
6782
isFlatpakRuntime,
83+
isSnapRuntime,
6884
mapFlatpakPathToSandbox
6985
}

electron/flatpak-paths.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const {
22
getFlatpakCompatRoots,
33
getSandboxSafePath,
4+
getSnapRealHome,
45
isFlatpakRuntime,
6+
isSnapRuntime,
57
mapFlatpakPathToSandbox
68
} = require('./flatpak-paths.cjs')
79

@@ -36,6 +38,27 @@ describe('flatpak path helpers', () => {
3638
).toBe('/home/alvaro/.config/PearPass')
3739
})
3840

41+
it('detects snap via SNAP_NAME', () => {
42+
expect(isSnapRuntime({ env: { SNAP_NAME: 'pearpass' } })).toBe(true)
43+
expect(isSnapRuntime({ env: {} })).toBe(false)
44+
})
45+
46+
it('returns SNAP_REAL_HOME inside snap, HOME otherwise', () => {
47+
expect(
48+
getSnapRealHome({
49+
env: {
50+
SNAP_NAME: 'pearpass',
51+
SNAP_REAL_HOME: '/home/alvaro',
52+
HOME: '/home/alvaro/snap/pearpass/current'
53+
}
54+
})
55+
).toBe('/home/alvaro')
56+
expect(getSnapRealHome({ env: { HOME: '/home/alvaro' } })).toBe(
57+
'/home/alvaro'
58+
)
59+
expect(getSnapRealHome({ env: {} })).toBe('')
60+
})
61+
3962
it('returns sandbox-safe path only when running inside flatpak', () => {
4063
const targetPath = '/home/alvaro/.var/app/com.pears.pass/data/PearPass'
4164

electron/main.cjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ let debugMode = false
3333
})()
3434

3535
const pkg = require('../package.json')
36-
const { getSandboxSafePath, isFlatpakRuntime } = require('./flatpak-paths.cjs')
36+
const {
37+
getSandboxSafePath,
38+
isFlatpakRuntime,
39+
isSnapRuntime
40+
} = require('./flatpak-paths.cjs')
3741
const runtimeConfig = require('./runtime-config.cjs')
3842
const {
3943
createMainProcessLogger
@@ -130,9 +134,9 @@ async function resolveRuntimeStorageDir() {
130134
let storageDir = getStorageDir()
131135
const linkId = upgrade.replace(/^pear:\/\//, '')
132136

133-
if (isFlatpakRuntime()) {
137+
if (isFlatpakRuntime() || isSnapRuntime()) {
134138
storageDir = path.join(storageDir, 'app-storage', 'by-dkey', linkId)
135-
logger.info('[MAIN]', 'Using Flatpak per-link storage root:', storageDir)
139+
logger.info('[MAIN]', 'Using sandbox per-link storage root:', storageDir)
136140
return storageDir
137141
}
138142

package-lock.json

Lines changed: 1607 additions & 560 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,30 +47,6 @@
4747
],
4848
"artifactName": "${productName}.${ext}"
4949
},
50-
"snap": {
51-
"base": "core22",
52-
"confinement": "classic",
53-
"grade": "stable",
54-
"summary": "A fully local, open-source password manager for privacy and control",
55-
"artifactName": "${productName}_${version}_${arch}.${ext}",
56-
"plugs": [
57-
"default",
58-
{
59-
"dot-config-pear": {
60-
"interface": "personal-files",
61-
"write": [
62-
"$HOME/.config/pear"
63-
]
64-
}
65-
},
66-
{
67-
"shmem": {
68-
"interface": "shared-memory",
69-
"private": true
70-
}
71-
}
72-
]
73-
},
7450
"toolsets": {
7551
"appimage": "1.0.2"
7652
},

scripts/build-snap.sh

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5+
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
SNAP_DIR="$PROJECT_DIR/snap"
7+
LOCAL_DIR="$SNAP_DIR/local"
8+
BUILD_DIR="$PROJECT_DIR/build/snap"
9+
10+
APPIMAGE_PATH=""
11+
UNPACKED_PATH=""
12+
ARCH=""
13+
VERSION=""
14+
SNAP_OUT=""
15+
16+
log_info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
17+
log_ok() { echo -e "\033[1;32m[OK]\033[0m $*"; }
18+
log_error() { echo -e "\033[1;31m[ERROR]\033[0m $*"; }
19+
20+
usage() {
21+
cat <<EOF
22+
Usage: $(basename "$0") (--unpacked <dir> | --local <AppImage>) [--arch arm64|x64]
23+
24+
Options:
25+
--unpacked <dir> Path to electron-builder's linux-*-unpacked directory
26+
(preferred — skips AppImage round-trip and avoids
27+
mksquashfs glibc issues on newer hosts).
28+
--local <path> Path to a locally-built AppImage. Used as a fallback
29+
when --unpacked is unavailable.
30+
--arch <arch> Target architecture (default: auto-detect)
31+
-h, --help Show this help message
32+
33+
Examples:
34+
$(basename "$0") --unpacked ./out/linux-arm64-unpacked
35+
$(basename "$0") --local ./out/PearPass.AppImage
36+
EOF
37+
exit 0
38+
}
39+
40+
detect_arch() {
41+
local machine
42+
machine="$(uname -m)"
43+
case "$machine" in
44+
x86_64) echo "amd64" ;;
45+
aarch64|arm64) echo "arm64" ;;
46+
*) log_error "Unsupported architecture: $machine"; exit 1 ;;
47+
esac
48+
}
49+
50+
check_prerequisites() {
51+
local missing=()
52+
command -v snapcraft >/dev/null 2>&1 || missing+=("snapcraft")
53+
54+
if (( ${#missing[@]} )); then
55+
log_error "Missing tools: ${missing[*]}"
56+
log_error "Install snapcraft: sudo snap install snapcraft --classic"
57+
exit 1
58+
fi
59+
60+
# snapcraft 8+ on core22 needs lxd (default) or multipass as a backend.
61+
if ! command -v lxd >/dev/null 2>&1 && ! command -v multipass >/dev/null 2>&1; then
62+
log_error "snapcraft needs an LXD or Multipass backend"
63+
log_error " sudo snap install lxd && sudo lxd init --auto && sudo usermod -aG lxd \$USER"
64+
log_error " (then log out and back in, or run: newgrp lxd)"
65+
exit 1
66+
fi
67+
68+
log_ok "Prerequisites satisfied"
69+
}
70+
71+
parse_args() {
72+
while [[ $# -gt 0 ]]; do
73+
case "$1" in
74+
--local) APPIMAGE_PATH="${2:?--local requires a path}"; shift 2 ;;
75+
--unpacked)
76+
# Bare --unpacked → auto-detect below. --unpacked <dir> → explicit.
77+
if [[ $# -ge 2 && "$2" != --* ]]; then
78+
UNPACKED_PATH="$2"; shift 2
79+
else
80+
shift 1
81+
fi
82+
;;
83+
--arch) ARCH="${2:?--arch requires a value}"; shift 2 ;;
84+
-h|--help) usage ;;
85+
*) log_error "Unknown option: $1"; usage ;;
86+
esac
87+
done
88+
89+
[[ -z "$ARCH" ]] && ARCH="$(detect_arch)"
90+
91+
if [[ -z "$UNPACKED_PATH" && -z "$APPIMAGE_PATH" ]]; then
92+
# Match electron-builder's naming: linux-arm64-unpacked for arm64,
93+
# linux-unpacked for x64.
94+
case "$ARCH" in
95+
arm64) arch_dir="linux-arm64-unpacked" ;;
96+
amd64|x64) arch_dir="linux-unpacked" ;;
97+
*) log_error "Unsupported arch for auto-detect: $ARCH"; exit 1 ;;
98+
esac
99+
for candidate in "$PROJECT_DIR/out/$arch_dir" "$PROJECT_DIR/dist/$arch_dir"; do
100+
if [[ -d "$candidate" ]]; then
101+
UNPACKED_PATH="$candidate"
102+
log_info "Auto-detected unpacked dir: $UNPACKED_PATH"
103+
break
104+
fi
105+
done
106+
fi
107+
108+
if [[ -z "$UNPACKED_PATH" && -z "$APPIMAGE_PATH" ]]; then
109+
log_error "Pass --unpacked <dir> or --local <AppImage>"
110+
usage
111+
fi
112+
113+
if [[ -n "$UNPACKED_PATH" ]]; then
114+
case "$UNPACKED_PATH" in /*) ;; *) UNPACKED_PATH="$PWD/$UNPACKED_PATH" ;; esac
115+
if [[ ! -d "$UNPACKED_PATH" ]]; then
116+
log_error "Unpacked directory not found: $UNPACKED_PATH"
117+
exit 1
118+
fi
119+
elif [[ -n "$APPIMAGE_PATH" ]]; then
120+
case "$APPIMAGE_PATH" in /*) ;; *) APPIMAGE_PATH="$PWD/$APPIMAGE_PATH" ;; esac
121+
if [[ ! -f "$APPIMAGE_PATH" ]]; then
122+
log_error "AppImage not found: $APPIMAGE_PATH"
123+
log_error "Build one first with: npm run dist:linux:<arch>"
124+
exit 1
125+
fi
126+
fi
127+
128+
VERSION="$(jq -r '.version' "$PROJECT_DIR/package.json")"
129+
log_info "Source : ${UNPACKED_PATH:-$APPIMAGE_PATH}"
130+
log_info "Arch : $ARCH"
131+
log_info "Version : $VERSION"
132+
}
133+
134+
stage_sources() {
135+
mkdir -p "$LOCAL_DIR"
136+
# Clean any previous staging so the picked branch in snapcraft.yaml is
137+
# deterministic (presence of unpacked/ wins over PearPass.AppImage).
138+
rm -rf "$LOCAL_DIR/unpacked" "$LOCAL_DIR/PearPass.AppImage"
139+
140+
if [[ -n "$UNPACKED_PATH" ]]; then
141+
log_info "Staging unpacked Electron payload into snap/local/unpacked/ ..."
142+
# Hard-link tree to avoid duplicating ~1 GB on the same filesystem.
143+
# Snapcraft copies sources into the build env, so it picks up files
144+
# by content; the link form just keeps the host disk footprint low.
145+
cp -al "$UNPACKED_PATH" "$LOCAL_DIR/unpacked"
146+
else
147+
log_info "Staging AppImage into snap/local/ ..."
148+
cp "$APPIMAGE_PATH" "$LOCAL_DIR/PearPass.AppImage"
149+
chmod +x "$LOCAL_DIR/PearPass.AppImage"
150+
fi
151+
}
152+
153+
build_snap() {
154+
log_info "Building snap (this may take several minutes on first run) ..."
155+
mkdir -p "$BUILD_DIR"
156+
157+
cd "$PROJECT_DIR"
158+
# Run pack from PROJECT_DIR. Two reasons we glob the result instead of
159+
# hard-coding the filename:
160+
# 1. `--output <abs-path>` is inconsistent across snapcraft+LXD versions
161+
# — snapcraft announces creation, but the host-side copy-back does
162+
# not always honor an absolute path outside PROJECT_DIR.
163+
# 2. snapcraft.yaml uses `adopt-info: pearpass` to pull the version
164+
# from flatpak/com.pears.pass.metainfo.xml, which can lag behind
165+
# package.json. Globbing the actual produced snap avoids the
166+
# mismatch.
167+
snapcraft pack
168+
local OUT
169+
OUT="$(ls -1t "$PROJECT_DIR"/pearpass_*_"${ARCH}".snap 2>/dev/null | head -n1)"
170+
if [[ -z "$OUT" || ! -f "$OUT" ]]; then
171+
log_error "snapcraft reported success but no pearpass_*_${ARCH}.snap was found in $PROJECT_DIR"
172+
exit 1
173+
fi
174+
SNAP_OUT="$BUILD_DIR/$(basename "$OUT")"
175+
mv -f "$OUT" "$SNAP_OUT"
176+
177+
log_ok "Snap bundle: $SNAP_OUT"
178+
ls -lh "$SNAP_OUT"
179+
}
180+
181+
cleanup() {
182+
log_info "Cleaning staging files ..."
183+
rm -rf "$LOCAL_DIR/unpacked"
184+
rm -f "$LOCAL_DIR/PearPass.AppImage"
185+
}
186+
187+
# ── Main ────────────────────────────────────────────────────────────────
188+
parse_args "$@"
189+
check_prerequisites
190+
stage_sources
191+
build_snap
192+
cleanup
193+
194+
SNAP_NAME="$(awk '/^name:/ {print $2; exit}' "$SNAP_DIR/snapcraft.yaml")"
195+
log_ok "Done!"
196+
log_info "Install with:"
197+
log_info " sudo snap install --dangerous $SNAP_OUT"
198+
log_info " sudo snap connect ${SNAP_NAME}:browser-native-messaging"

0 commit comments

Comments
 (0)