notion-app-electron-bin

LOW
maintainer Ishan-Parihar 0 votes scanned 2026-10-30 03:55:04.906523
View on AUR
Why flagged

The package repacks Notion's official Windows installer by extracting and patching it for Linux, verifies the download with a checksum from Notion's official manifest, and builds the app in a reproducible way; the only concerns are the lack of source integrity for bundled local files (SKIP'd checksums) and the project's newness/low votes, but no malicious or risky remote code execution is present.

Triggered rules

Low Few votes, recently uploaded zero_votes_recent

Uploaded within the last 14 days with 2 or fewer community votes — little peer review so far.

Low AI review llm_review

An AI model (qwen/qwen3-235b-a22b-2507) reviewed this and agrees it is LOW (confidence 95%): The package repacks Notion's official Windows installer by extracting and patching it for Linux, verifies the download with a checksum from Notion's official manifest, and builds the app in a reproducible way; the only concerns are the lack of source integrity for bundled local files (SKIP'd checksums) and the project's newness/low votes, but no malicious or risky remote code execution is present.

PKGBUILD

1# Maintainer: Ishan Parihar <ishanparihar at outlook dot com>
2#
3# Repack of the official Notion desktop app for Linux.
4#
5# Notion ships Windows and macOS builds only; there is no official Linux
6# release. This package repacks the Windows NSIS installer, applies the
7# Linux patches, and runs it on Arch's system Electron.
8#
9# The point of this base, versus notion-app-electron, is that nothing here
10# needs a human on an upstream bump:
11#
12# pkgver() resolves the version from Notion's own latest.yml
13# prepare() fetches that version, verifies it against the sha512
14# published in the same manifest, then extracts it
15# native deps better-sqlite3 and node-pipewire versions are read from
16# the app's own package.json instead of being pinned here
17#
18# So `yay -Syu` picks up a new Notion release without a PKGBUILD edit.
19
20pkgname=notion-app-electron-bin
21pkgver=7.41.1 # fallback; overridden by pkgver()
22pkgrel=1
23pkgdesc="Your connected workspace for wiki, docs & projects (official app, repacked)"
24arch=(x86_64)
25url="https://www.notion.so/desktop"
26license=(custom:proprietary)
27
28depends=(
29 bash
30 electron43
31 glibc
32 gcc-libs
33 hicolor-icon-theme
34 libpipewire
35)
36
37makedepends=(
38 7zip
39 asar
40 ast-grep
41 curl
42 jq
43 libarchive
44 nodejs
45 python
46)
47
48provides=("notion-app-electron=$pkgver")
49# Same product, same launcher: replace the hand-maintained package rather
50# than installing a second copy of Notion. Deliberately NOT conflicting
51# with notion-app-enhanced, which provides "notion-app" (a different
52# name), so there is no provider ambiguity and no forced removal there.
53conflicts=(notion-app-electron)
54
55options=(!emptydirs !strip)
56install=notion-app-electron-bin.install
57
58# The installer is fetched by prepare(), not by source=, because prepare()
59# runs BEFORE pkgver() and the artefact name contains the version. A
60# source=() entry would expand the stale fallback.
61
62_manifest="https://desktop-release.notion-static.com/latest.yml"
63
64# Resolve the upstream version. Notion's manifest is plain yaml served
65# without auth and without rate limits, and it carries the sha512 of the
66# same artefact, so prepare() can verify its download without trusting a
67# second source of truth.
68pkgver() {
69 local _v
70 _v=$(curl -fsSL "$_manifest" 2>/dev/null | sed -n 's/^version: *//p' | head -1)
71 [[ -n "$_v" ]] && printf '%s' "$_v" || printf '%s' "7.41.1"
72}
73
74# The manifest publishes the sha512 base64-encoded; makepkg and sha512sum
75# both want hex.
76_manifest_sha512_hex() {
77 curl -fsSL "$_manifest" 2>/dev/null \
78 | sed -n 's/^sha512: *//p' | head -1 \
79 | base64 -d 2>/dev/null \
80 | od -An -tx1 | tr -d ' \n'
81}
82
83# Read one dependency version out of the extracted app's package.json.
84# This is what keeps the native modules matched to the app version.
85_app_dep_version() {
86 python - "$1" "$2" <<'PY'
87import json, sys
88path, field = sys.argv[1], sys.argv[2]
89with open(path) as fh:
90 data = json.load(fh)
91for section in ("dependencies", "optionalDependencies"):
92 got = (data.get(section) or {}).get(field)
93 if got:
94 print(got)
95 break
96PY
97}
98
99source=(
100 notion-app
101 notion.desktop
102 notion.png
103)
104sha256sums=('SKIP' 'SKIP' 'SKIP')
105
106prepare() {
107 cd "$srcdir"
108
109 local _ver _installer _want _got _encoded
110 _ver=$(pkgver)
111 _installer="Notion Setup ${_ver}.exe"
112
113 # --- fetch the installer, verified against the manifest ----------------
114 if [[ -f "$_installer" ]] && 7z t "$_installer" >/dev/null 2>&1; then
115 msg2 "using cached %s" "$_installer"
116 else
117 msg2 "fetching Notion %s" "$_ver"
118 _want=$(_manifest_sha512_hex)
119 [[ -n "$_want" ]] || { error "could not read a sha512 from the manifest"; return 1; }
120
121 _encoded=$(printf '%s' "$_installer" | sed 's/ /%20/g')
122 if ! curl -fL --retry 3 -o "$_installer" \
123 "https://desktop-release.notion-static.com/${_encoded}"; then
124 error "download failed for %s" "$_installer"
125 return 1
126 fi
127
128 _got=$(sha512sum "$_installer" | cut -d' ' -f1)
129 if [[ "$_got" != "$_want" ]]; then
130 error "sha512 mismatch"
131 error " expected %s" "$_want"
132 error " got %s" "$_got"
133 rm -f "$_installer"
134 return 1
135 fi
136 msg2 "sha512 verified against latest.yml"
137 fi
138
139 # --- unpack the NSIS payload and the asar ------------------------------
140 # Discard the previous extraction so a file upstream removed cannot
141 # survive into the new package.
142 rm -rf "$srcdir/asar_patched" "$srcdir/resources" "${srcdir:?}/\$PLUGINSDIR"
143 # 7z returns non-zero on the NSIS installer's trailing data even when the
144 # payload extracts cleanly, so the status is only advisory here. The
145 # app.asar existence check below is the real assertion.
146 7z x "$_installer" '$PLUGINSDIR/app-64.7z' -y -bse0 -bso0 || true
147 7z x '$PLUGINSDIR/app-64.7z' 'resources/app.asar' 'resources/app.asar.unpacked' -y -bse0 -bso0 || true
148 [[ -f resources/app.asar ]] || { error "app.asar not found inside the installer"; return 1; }
149
150 asar e resources/app.asar asar_patched \
151 || { error "asar extraction failed"; return 1; }
152
153 # --- match the native modules to the versions the app expects ---------
154 local _sqlite_ver _pipewire_ver _pw_dir
155 _sqlite_ver=$(_app_dep_version asar_patched/package.json better-sqlite3)
156 _pipewire_ver=$(_app_dep_version asar_patched/package.json node-pipewire)
157 [[ -n "$_sqlite_ver" ]] || { error "could not read better-sqlite3 from package.json"; return 1; }
158 [[ -n "$_pipewire_ver" ]] || { error "could not read node-pipewire from package.json"; return 1; }
159 msg2 "app expects better-sqlite3 %s and node-pipewire %s" "$_sqlite_ver" "$_pipewire_ver"
160
161 # better-sqlite3: the Linux prebuild ships inside the npm tarball.
162 curl -fsSL -o "$srcdir/sqlite3.tgz" \
163 "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-${_sqlite_ver}.tgz" \
164 || { error "could not fetch better-sqlite3 %s" "$_sqlite_ver"; return 1; }
165 bsdtar -xOf "$srcdir/sqlite3.tgz" package/prebuilds/linux-x64.node \
166 > "$srcdir/asar_patched/node_modules/better-sqlite3/build/Release/better_sqlite3.node" \
167 || { error "better-sqlite3 has no linux-x64 prebuild"; return 1; }
168
169 # node-pipewire: the ESM wrapper from npm plus the Node-API prebuild.
170 _pw_dir="$srcdir/asar_patched/node_modules/node-pipewire"
171 install -d "$_pw_dir/dist/binding/napi-v6"
172 curl -fsSL -o "$srcdir/pipewire.tgz" \
173 "https://registry.npmjs.org/node-pipewire/-/node-pipewire-${_pipewire_ver}.tgz" \
174 || { error "could not fetch node-pipewire %s" "$_pipewire_ver"; return 1; }
175 bsdtar -xf "$srcdir/pipewire.tgz" -C "$_pw_dir" --strip-components 1 \
176 package/package.json package/dist/index.js \
177 || { error "node-pipewire tarball layout changed"; return 1; }
178 curl -fsSL -o "$srcdir/pipewire-prebuilt.tar.gz" \
179 "https://github.com/kakxem/node-pipewire/releases/download/${_pipewire_ver}/napi-v6-linux-x64.tar.gz" \
180 || { error "could not fetch the node-pipewire prebuild"; return 1; }
181 bsdtar -xOf "$srcdir/pipewire-prebuilt.tar.gz" napi-v6/index.node \
182 > "$_pw_dir/dist/binding/napi-v6/index.node" \
183 || { error "node-pipewire prebuild has an unexpected layout"; return 1; }
184
185 # --- drop the artefacts that cannot load on Linux ---------------------
186 rm -f \
187 "asar_patched/.webpack/main/native_modules/build/Release/winregistry.node" \
188 "asar_patched/.webpack/main/native_modules/adjust_get_current_system_volume_vista_plus.exe" \
189 "asar_patched/node_modules/@notionhq/desktop-native/build/Release/win_utils.node" \
190 "asar_patched/node_modules/better-sqlite3/build/Release/test_extension.node"
191 rm -rf \
192 "asar_patched/node_modules/cf-prefs" \
193 "asar_patched/node_modules/node-mac-window"
194 # progress_bar.node's Linux build links GTK4 while system Electron links
195 # GTK3; loading both aborts the process. Notion falls back gracefully.
196 rm -f "asar_patched/node_modules/@jasonscheirer/native-progress-bar/build/Release/progress_bar.node"
197
198 # getIcon() is patched to read the tray icon from __dirname, so the icon
199 # has to sit next to the bundle.
200 install -Dm644 "$srcdir/notion.png" "asar_patched/.webpack/main/trayIcon.png"
201
202 # --- the Linux patches ------------------------------------------------
203 local _index="$srcdir/asar_patched/.webpack/main/index.js"
204 [[ -f "$_index" ]] || { error "main bundle missing at %s" "$_index"; return 1; }
205
206 # ast-grep exits 1 when a pattern matches nothing, and makepkg runs the
207 # PKGBUILD under `set -e`, so a stale anchor aborts the build here rather
208 # than silently shipping an unpatched app.
209 _sg_patch() {
210 ast-grep run --lang javascript -U -p "$1" -r "$2" "$_index" >/dev/null 2>&1 \
211 || { error "patch did not apply: %.60s" "$1"; return 1; }
212 }
213 _sg_patch_select() {
214 ast-grep run --lang javascript -U --selector "$1" -p "$2" -r "$3" "$_index" >/dev/null 2>&1 \
215 || { error "patch did not apply: %.60s" "$2"; return 1; }
216 }
217
218 # 1. the tray menu never opened: attach the context menu first
219 _sg_patch \
220 'this.tray.on("click",()=>{this.onClick()})' \
221 'this.tray.setContextMenu(this.trayMenu),this.tray.on("click",()=>{this.onClick()})'
222 # 2. the tray icon was blank: resolve it from disk
223 _sg_patch_select method_definition \
224 'class X { getIcon(){ $$$BODY } }' \
225 'getIcon(){return require("path").resolve(__dirname,"trayIcon.png");}'
226 # 3. the service worker did not register: present as Windows
227 _sg_patch \
228 '$S.setUserAgent(`${$S.getUserAgent()} WantsServiceWorker`)' \
229 '$S.setUserAgent(`${$S.getUserAgent().replace("Linux", "Windows")} WantsServiceWorker`)' \
230 # 4. the uninstall handler was Windows-only
231 _sg_patch \
232 'if("darwin"===process.platform)$MAC;else if("win32"===process.platform){const $UNINSTALL=$ARGS=>$ARGS.find($ARG=>"--uninstall"===$ARG);$$$B}' \
233 'if("darwin"===process.platform)$MAC;else if("linux"===process.platform){const $UNINSTALL=$ARGS=>$ARGS.find($ARG=>"--uninstall"===$ARG);$$$B}'
234 # 5. system Electron puts the asar in argv; do not treat it as a file
235 _sg_patch \
236 'function $F($ARG){const $EXT=$PATH.default.extname($ARG).toLowerCase();return!$ARG.startsWith("-")&&void 0===$SCHEME($ARG)&&$EXT.length>0&&".exe"!==$EXT}' \
237 'function $F($ARG){const $EXT=$PATH.default.extname($ARG).toLowerCase();return!$ARG.startsWith("-")&&void 0===$SCHEME($ARG)&&$EXT.length>0&&".exe"!==$EXT&&".asar"!==$EXT}'
238 # 6. a comma-expression left a bare 0 where a statement belonged
239 _sg_patch \
240 '($$$PRE,function(){$$$INIT}(),0)' \
241 '($$$PRE,function(){$$$INIT}())'
242 # 7. the win32 branch was taking the Linux code path
243 _sg_patch_select ternary_expression \
244 '"win32"===process.platform?function($E,$T){$$$A}($E,$T):$ALT' \
245 '"linux"===process.platform?function($E,$T){$$$A}($E,$T):$ALT'
246
247 # --- verify every patch landed ----------------------------------------
248 # A pattern can match and still produce the wrong result, so assert the
249 # patched form is present and the original is gone before packing.
250 #
251 # NOTE: the patterns above use ast-grep metavariables ($EXT, $$$INIT,
252 # $S, ...). Those are substituted with the matched source text, so the
253 # metavariable *names* never appear in the output. The assertions below
254 # therefore check the literal text that survives the substitution, not
255 # the pattern syntax.
256 local _fail=0
257 _check() {
258 if ! grep -qF "$2" "$_index"; then
259 error "verify %s: patched form MISSING" "$1"; _fail=1
260 fi
261 if [[ -n "${3:-}" ]] && grep -qF "$3" "$_index"; then
262 error "verify %s: original form STILL PRESENT" "$1"; _fail=1
263 fi
264 }
265 _check tray-context-menu 'setContextMenu(this.trayMenu),this.tray.on("click"'
266 _check tray-icon 'trayIcon.png'
267 _check user-agent 'replace("Linux", "Windows")'
268 _check uninstall-linux 'else if("linux"===process.platform)'
269 # `".asar"!==` only exists after the patch; the original ended at `.exe"!==`.
270 _check argv-extension '".asar"!=='
271 # The comma-expression patch drops the `,0` and keeps the trailing `()`.
272 # `}(),0)` is the unpatched form.
273 _check comma-expression 'function(){return arguments}()' '}(),0)'
274 [[ $_fail -eq 0 ]] || { error "one or more patches did not land"; return 1; }
275 msg2 "all patches verified"
276
277 # The bundle must parse with the parser that will actually load it.
278 # node --check understands the ESM + top-level await that python rejects.
279 node --check "$_index" || { error "the patched bundle does not parse"; return 1; }
280
281 asar p asar_patched "$srcdir/app.asar" --unpack '*.node' \
282 || { error "asar repack failed"; return 1; }
283}
284
285check() {
286 cd "$srcdir"
287
288 # 1. the app identifies itself as the version we packaged
289 grep -q "\"version\": \"$pkgver\"" asar_patched/package.json \
290 || { error "package.json does not report %s" "$pkgver"; return 1; }
291
292 # 2. the patches are in the payload we are about to ship, not only in the
293 # staging tree (checking the staging tree proves we edited something;
294 # checking the packed asar proves we shipped it)
295 asar e app.asar .verify || { error "could not re-extract app.asar"; return 1; }
296 local _v=.verify/.webpack/main/index.js
297 grep -qF 'setContextMenu(this.trayMenu)' "$_v" \
298 || { error "tray patch missing from the packed app.asar"; return 1; }
299 grep -qF '".asar"!==' "$_v" \
300 || { error "argv patch missing from the packed app.asar"; return 1; }
301
302 # 3. the native modules the app will dlopen are present and non-empty
303 local _node
304 for _node in \
305 asar_patched/node_modules/better-sqlite3/build/Release/better_sqlite3.node \
306 asar_patched/node_modules/node-pipewire/dist/binding/napi-v6/index.node
307 do
308 [[ -s "$_node" ]] || { error "native module missing or empty: %s" "$_node"; return 1; }
309 done
310
311 # 4. it actually starts and reports readiness.
312 #
313 # Notion is a persistent desktop app: it does not exit on its own, so the
314 # old "wait for the process to die" logic was wrong in two ways. It gave
315 # up before the app had finished booting on a slow machine, and it
316 # treated a still-running process as a failure.
317 #
318 # The right assertion is "the app printed its version line and did not
319 # print a fatal error". So: start it, poll the log for the version line,
320 # then stop it again.
321 #
322 # --headless alone dumps core here; the Ozone platform has to be forced
323 # to headless as well, or the process tries to reach an X server that
324 # does not exist and exits before it can log anything.
325 local _log="$srcdir/smoke.log" _pid _i _probe_datadir
326 : > "$_log"
327 # A private user-data dir. Electron's single-instance lock lives in the
328 # default one, so launching against $HOME would either fail because the
329 # user already has Notion open, or leave a lock behind pointing at this
330 # pid that the user's next real launch then trusts -- which manifests as
331 # the app exiting silently and never showing a window. An isolated dir
332 # makes the check self-contained either way.
333 _probe_datadir="$srcdir/.smoke-profile"
334 rm -rf "${_probe_datadir:?}"
335 env -u DISPLAY -u WAYLAND_DISPLAY electron43 --headless --no-sandbox \
336 --ozone-platform=headless \
337 --user-data-dir="$_probe_datadir" \
338 "$srcdir/app.asar" >"$_log" 2>&1 &
339 _pid=$!
340
341 _ready=0
342 for _i in $(seq 1 60); do
343 # The version line is the app's own "I booted" signal.
344 if grep -q "App starting with version $pkgver" "$_log"; then
345 _ready=1
346 break
347 fi
348 # If it died before reporting, there is no point waiting further.
349 kill -0 "$_pid" 2>/dev/null || break
350 sleep 0.5
351 done
352
353 # Stop it again. SIGTERM makes wait return 143, which would abort under
354 # set -e, so the status is discarded.
355 kill -TERM "$_pid" 2>/dev/null || true
356 wait "$_pid" 2>/dev/null || true
357
358 [[ $_ready -eq 1 ]] \
359 || { error "the app did not report version %s within 30s" "$pkgver"; cat "$_log" >&2; return 1; }
360
361 # 5. the log says it started, and says nothing fatal
362 if grep -qiE "uncaught|unhandled rejection|SyntaxError|Cannot find module" "$_log"; then
363 error "the startup log reports a fatal error"
364 cat "$_log" >&2
365 return 1
366 fi
367
368 rm -rf "${_probe_datadir:?}"
369 msg2 "smoke test passed: started as %s with no fatal errors" "$pkgver"
370}
371
372package() {
373 local _lib="$pkgdir/usr/lib/notion-app"
374 local _share="$pkgdir/usr/share"
375
376 install -d "$_lib"
377 install -Dm644 "$srcdir/app.asar" "$_lib/app.asar"
378 if [[ -d "$srcdir/app.asar.unpacked" ]]; then
379 cp -a "$srcdir/app.asar.unpacked" "$_lib/"
380 fi
381 install -Dm644 "$srcdir/asar_patched/package.json" "$_lib/package.json"
382
383 install -Dm755 "$srcdir/notion-app" "$pkgdir/usr/bin/notion-app"
384 install -Dm644 "$srcdir/notion.desktop" "$_share/applications/notion-app.desktop"
385 install -Dm644 "$srcdir/notion.png" "$_share/icons/hicolor/512x512/apps/notion-app.png"
386
387 install -d "$pkgdir/usr/share/licenses/$pkgname"
388 cat > "$pkgdir/usr/share/licenses/$pkgname/LICENSE" <<EOF
389Proprietary. Notion is copyright Notion Labs, Inc.
390This package only repacks the upstream desktop build so it runs on Linux.
391See $url for the terms.
392EOF
393}
394

Scan history

Scanned at (UTC)SeverityRules
2026-10-30 03:55:04 Low 2

Report a package

Reports go to the AURWatch maintainer (one person) and are read by hand. No login required.

0 / 4000
Your suggestion