Skip to content
Open
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
13 changes: 13 additions & 0 deletions pkgbuilds/granola/.omarchy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"source": "local",
"release_ring": "fast",
"upstream": {
"watch": {
"json": "https://api.granola.ai/v1/get-versions",
"path": "production"
}
},
"origin": {
"aur": "granola"
}
}
180 changes: 180 additions & 0 deletions pkgbuilds/granola/PKGBUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Maintainer: Shawn Yeager <shawn@shawnyeager.com>
#
# Repackages the official Granola macOS .dmg as a native Linux app, using
# distro electron44. Conversion follows https://github.com/tirtha4/Granola-for-Linux
# (and the AUR granola recipe derived from it). x86_64 only: the native
# module is built for linux-x64.
#
# Source is the versioned CloudFront URL Granola documents as the static CDN
# target of api.granola.ai/v1/download-latest. There is no GitHub release.
# Version discovery uses api.granola.ai/v1/get-versions.
#
# Format: shfmt -ln bash -i 2 -d PKGBUILD

pkgname=granola
pkgver=7.568.0
pkgrel=1
pkgdesc="AI notepad for meetings, repackaged from the official macOS build"
arch=('x86_64')
url="https://www.granola.ai"
license=('LicenseRef-proprietary')
_electron=electron44
# Must match the installed $_electron package (pacman -Q $_electron).
_elver=44.3.0
# Must match the fork version inside the .dmg.
_bs3ver=12.9.0
depends=(
"$_electron"
'libstdc++'
'glibc'
'sh'
'desktop-file-utils'
'xdg-utils'
)
conflicts=('granola-wine')
makedepends=(
'7zip'
'gcc'
'make'
'nodejs'
'node-gyp'
'python'
)
options=('!debug')
source=(
"granola-$pkgver.dmg::https://dr2v7l5emb758.cloudfront.net/$pkgver/Granola-$pkgver-mac-universal.dmg"
"https://registry.npmjs.org/better-sqlite3-multiple-ciphers/-/better-sqlite3-multiple-ciphers-$_bs3ver.tgz"
"https://electronjs.org/headers/v$_elver/node-v$_elver-headers.tar.gz"
"granola.desktop"
)
noextract=(
"granola-$pkgver.dmg"
"node-v$_elver-headers.tar.gz"
)
sha256sums=('29fabbacdbad860be46e434b39fdbbf424982affed39f93e0e47899532784b73'
'ad8ceb2cfe687e0c106547fdd281f0d20b40688200d04f40bb163afd1f102609'
'ce3ef7604c6a19f6c8e018a771919cacf4a85cea56ddfaa850335908fea83377'
'20c443a00fcba28a00292aab36eda376af28e2b6d2f63e083221edc1af7e3f18')

_res="Granola/Granola.app/Contents/Resources"
_bs3rel="app.asar.unpacked/node_modules/better-sqlite3-multiple-ciphers"

_7z() {
if command -v 7zz >/dev/null; then
7zz "$@"
else
7z "$@"
fi
}

prepare() {
local dmg="$srcdir/granola-$pkgver.dmg"

local sysver
sysver="$(</usr/lib/$_electron/version)"
if [[ "$sysver" != "$_elver" ]]; then
echo "installed $_electron is $sysver, set _elver=$sysver and run updpkgsums" >&2
return 1
fi

_7z e "$dmg" \
"Granola/Granola.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources/Info.plist" \
-ofw -y >/dev/null || { echo "cannot read the .dmg" >&2; return 1; }
local dmgel
dmgel="$(grep -A1 CFBundleVersion fw/Info.plist | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
if [[ "${dmgel%%.*}" != "${_electron#electron}" ]]; then
echo ".dmg bundles Electron $dmgel, set _electron=electron${dmgel%%.*} and depends to match" >&2
return 1
fi

_7z x "$dmg" "$_res/app.asar" "$_res/app.asar.unpacked" "$_res/icons" \
-opayload -y >/dev/null

local bs3="payload/$_res/$_bs3rel"
local realver
realver="$(node -p "require('$srcdir/$bs3/package.json').version")"
if [[ "$realver" != "$_bs3ver" ]]; then
echo "bundled better-sqlite3-multiple-ciphers is $realver, set _bs3ver=$realver and run updpkgsums" >&2
return 1
fi
cp package/binding.gyp "$bs3/"

# api.granola.ai answers 500 to any request carrying platform=linux,
# including the sign-in URL. The app maps darwin->macOS and win32->Windows
# and passes anything else through verbatim; rewrite that fallback so Linux
# reports Windows. The replacement is padded to the same byte length
# because the asar header records file offsets.
python - "payload/$_res/app.asar" <<'PYEOF'
import sys, pathlib
p = pathlib.Path(sys.argv[1]); data = p.read_bytes(); total = 0
for pat in (b'?`Windows`:window.electron.platform', b'?`Windows`:process.platform'):
rep = b'?`Windows`:`Windows`'.ljust(len(pat))
total += data.count(pat)
data = data.replace(pat, rep)
if total == 0:
sys.exit("no platform fallback found; Granola's bundler output changed")
p.write_bytes(data)
print(f"patched {total} platform fallback(s)")
PYEOF
}

build() {
cd "payload/$_res/$_bs3rel" || exit 1
node-gyp rebuild --release --runtime=electron --target="$_elver" --arch=x64 \
--dist-url=https://electronjs.org/headers \
--tarball="$srcdir/node-v$_elver-headers.tar.gz" \
--devdir="$srcdir/gyp-devdir"
}

check() {
local bs3="$srcdir/payload/$_res/$_bs3rel"
local db="$srcdir/smoke.db"
rm -f "$db"
ELECTRON_RUN_AS_NODE=1 NODE_PATH="$srcdir/payload/$_res/app.asar/node_modules" \
"/usr/lib/$_electron/electron" -e "
const Database = require('$bs3/lib/index.js');
const db = new Database('$db');
db.pragma(\"cipher='sqlcipher'\");
db.pragma(\"key='smoketest'\");
db.exec('CREATE TABLE t(a)');
let fired = false;
db.updateHook(() => { fired = true; });
db.prepare('INSERT INTO t VALUES (1)').run();
if (db.prepare('SELECT count(*) c FROM t').get().c !== 1) throw new Error('insert failed');
if (!fired) throw new Error('updateHook did not fire');
db.close();
"
rm -f "$db"
}

package() {
install -Dm644 "payload/$_res/app.asar" "$pkgdir/usr/lib/granola/app.asar"
cp -r "payload/$_res/app.asar.unpacked" "$pkgdir/usr/lib/granola/app.asar.unpacked"

# Keep only the built binaries from the module tree; drop the C++ source,
# sqlite amalgamation, and node-gyp intermediates.
local bs3="$pkgdir/usr/lib/granola/$_bs3rel"
install -Dm755 "$bs3"/build/Release/*.node -t "$srcdir/keep"
rm -rf "$bs3/build" "$bs3/deps" "$bs3/src" "$bs3/binding.gyp"
install -Dm755 "$srcdir/keep"/*.node -t "$bs3/build/Release"

# icon.png is a black "g|" wordmark on a transparent canvas. mac-icon.png
# is the lime squircle Granola actually ships.
if [[ -f "payload/$_res/icons/mac-icon.png" ]]; then
install -Dm644 "payload/$_res/icons/mac-icon.png" "$pkgdir/usr/share/pixmaps/granola.png"
else
install -Dm644 "payload/$_res/icons/icon.png" "$pkgdir/usr/share/pixmaps/granola.png"
fi
install -Dm644 granola.desktop "$pkgdir/usr/share/applications/granola.desktop"

install -Dm755 /dev/stdin "$pkgdir/usr/bin/granola" <<EOF
#!/bin/sh
exec /usr/lib/$_electron/electron --ozone-platform-hint=auto --enable-features=WebRTCPipeWireCapturer /usr/lib/granola/app.asar "\$@"
EOF

install -Dm644 /dev/stdin "$pkgdir/usr/share/licenses/$pkgname/LICENSE" <<EOF
Granola is proprietary software. Copyright (c) Granola.
This package repackages the official binary distribution; no license text
is shipped inside it. Terms: https://www.granola.ai/terms
EOF
}
13 changes: 13 additions & 0 deletions pkgbuilds/granola/granola.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Granola
Comment=AI Notepad for meetings
Exec=granola %U
Icon=granola
Terminal=false
Categories=Office;
Keywords=meetings;notes;transcript;notepad;
StartupNotify=true
StartupWMClass=granola
MimeType=x-scheme-handler/granola;