_1password-gui: Add passthru.updateScript (#416072)

This commit is contained in:
Austin Horstman
2025-06-16 08:28:55 -05:00
committed by GitHub
7 changed files with 297 additions and 118 deletions
@@ -30,4 +30,6 @@ stdenv.mkDerivation {
# 1Password is notarized.
dontFixup = true;
passthru.updateScript = ./update.sh;
}
+2
View File
@@ -153,4 +153,6 @@ stdenv.mkDerivation {
# https://1password.community/discussion/comment/624011/#Comment_624011
#--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}"
'';
passthru.updateScript = ./update.sh;
}
+15 -10
View File
@@ -11,18 +11,18 @@
let
pname = "1password";
versions = builtins.fromJSON (builtins.readFile ./versions.json);
hostOs = if stdenv.hostPlatform.isLinux then "linux" else "darwin";
version = versions."${channel}-${hostOs}" or (throw "unknown channel-os ${channel}-${hostOs}");
hostOs = stdenv.hostPlatform.parsed.kernel.name;
hostArch = stdenv.hostPlatform.parsed.cpu.name;
sources = builtins.fromJSON (builtins.readFile ./sources.json);
sourcesChan = sources.${channel} or (throw "unsupported channel ${channel}");
sourcesChanOs = sourcesChan.${hostOs} or (throw "unsupported OS ${hostOs}");
sourcesChanOsArch =
sourcesChanOs.sources.${hostArch} or (throw "unsupported architecture ${hostArch}");
inherit (sourcesChanOs) version;
src = fetchurl {
inherit
(sources.${channel}.${stdenv.system} or (throw "unsupported system ${stdenv.hostPlatform.system}"))
url
hash
;
inherit (sourcesChanOsArch) url hash;
};
meta = {
@@ -37,7 +37,12 @@ let
sebtm
bdd
];
platforms = builtins.attrNames sources.${channel};
platforms = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
];
mainProgram = "1password";
};
+48 -28
View File
@@ -1,38 +1,58 @@
{
"stable": {
"x86_64-linux": {
"url": "https://downloads.1password.com/linux/tar/stable/x86_64/1password-8.10.78.x64.tar.gz",
"hash": "sha256-COmXSjbCetPsbm40OrWGVtULPheEgnHEO0ZcIgWaG1w="
"linux": {
"version": "8.10.78",
"sources": {
"x86_64": {
"url": "https://downloads.1password.com/linux/tar/stable/x86_64/1password-8.10.78.x64.tar.gz",
"hash": "sha256-COmXSjbCetPsbm40OrWGVtULPheEgnHEO0ZcIgWaG1w="
},
"aarch64": {
"url": "https://downloads.1password.com/linux/tar/stable/aarch64/1password-8.10.78.arm64.tar.gz",
"hash": "sha256-diy7VhKRluSnVSR35Ogamf9RDHdqxSJifLOOYmMrJHE="
}
}
},
"aarch64-linux": {
"url": "https://downloads.1password.com/linux/tar/stable/aarch64/1password-8.10.78.arm64.tar.gz",
"hash": "sha256-diy7VhKRluSnVSR35Ogamf9RDHdqxSJifLOOYmMrJHE="
},
"x86_64-darwin": {
"url": "https://downloads.1password.com/mac/1Password-8.10.78-x86_64.zip",
"hash": "sha256-8fbjEc/Z0xCdXq/uHp4bQE5Js5hNLbVCRZxnepUdLUs="
},
"aarch64-darwin": {
"url": "https://downloads.1password.com/mac/1Password-8.10.78-aarch64.zip",
"hash": "sha256-x03dZ/eVrvFcbese1cBAvyJKwtWe6rOcgytn0OsEFDQ="
"darwin": {
"version": "8.10.78",
"sources": {
"x86_64": {
"url": "https://downloads.1password.com/mac/1Password-8.10.78-x86_64.zip",
"hash": "sha256-8fbjEc/Z0xCdXq/uHp4bQE5Js5hNLbVCRZxnepUdLUs="
},
"aarch64": {
"url": "https://downloads.1password.com/mac/1Password-8.10.78-aarch64.zip",
"hash": "sha256-x03dZ/eVrvFcbese1cBAvyJKwtWe6rOcgytn0OsEFDQ="
}
}
}
},
"beta": {
"x86_64-linux": {
"url": "https://downloads.1password.com/linux/tar/beta/x86_64/1password-8.10.80-18.BETA.x64.tar.gz",
"hash": "sha256-X2Wu/dQQ7fv+tTAU2/70S38wL6WdJuc/DXWoiHZvSP4="
"linux": {
"version": "8.10.80-18.BETA",
"sources": {
"x86_64": {
"url": "https://downloads.1password.com/linux/tar/beta/x86_64/1password-8.10.80-18.BETA.x64.tar.gz",
"hash": "sha256-X2Wu/dQQ7fv+tTAU2/70S38wL6WdJuc/DXWoiHZvSP4="
},
"aarch64": {
"url": "https://downloads.1password.com/linux/tar/beta/aarch64/1password-8.10.80-18.BETA.arm64.tar.gz",
"hash": "sha256-52aRg6QD/fKOzOHoG88q8VNJIizxnISFnpxek7bJ05w="
}
}
},
"aarch64-linux": {
"url": "https://downloads.1password.com/linux/tar/beta/aarch64/1password-8.10.80-18.BETA.arm64.tar.gz",
"hash": "sha256-52aRg6QD/fKOzOHoG88q8VNJIizxnISFnpxek7bJ05w="
},
"x86_64-darwin": {
"url": "https://downloads.1password.com/mac/1Password-8.10.80-18.BETA-x86_64.zip",
"hash": "sha256-kUU+nm19DmdY8ZG6d+EJFQXcCy/BOauXh83suQLSvz0="
},
"aarch64-darwin": {
"url": "https://downloads.1password.com/mac/1Password-8.10.80-18.BETA-aarch64.zip",
"hash": "sha256-eZG0QaB5NRwRCYcmlfZA/HTceLq7eUzR+AvzDeOrzAY="
"darwin": {
"version": "8.10.80-18.BETA",
"sources": {
"x86_64": {
"url": "https://downloads.1password.com/mac/1Password-8.10.80-18.BETA-x86_64.zip",
"hash": "sha256-kUU+nm19DmdY8ZG6d+EJFQXcCy/BOauXh83suQLSvz0="
},
"aarch64": {
"url": "https://downloads.1password.com/mac/1Password-8.10.80-18.BETA-aarch64.zip",
"hash": "sha256-eZG0QaB5NRwRCYcmlfZA/HTceLq7eUzR+AvzDeOrzAY="
}
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3 gnupg
import json
import os
import shutil
import subprocess
import sys
import tempfile
from collections import OrderedDict
DOWNLOADS_BASE_URL = "https://downloads.1password.com"
OP_PGP_KEYID = "3FEF9748469ADBE15DA7CA80AC2D62742012EA22"
class Sources(OrderedDict):
def __init__(self):
self._jsonfp = open("sources.json", "r+")
self.update(json.load(self._jsonfp))
self._jsonfp.seek(0, os.SEEK_SET)
def persist(self):
json.dump(self, self._jsonfp, indent=2)
self._jsonfp.write("\n") # keep fmt.check happy
class GPG:
def __new__(cls):
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if hasattr(self, "gnupghome"):
return
self.gpg = shutil.which("gpg")
self.gpgv = shutil.which("gpgv")
self.gnupghome = tempfile.mkdtemp(prefix="1password-gui-gnupghome.")
self.env = {"GNUPGHOME": self.gnupghome}
self._run(
self.gpg,
"--no-default-keyring",
"--keyring",
"trustedkeys.kbx",
"--keyserver",
"keyserver.ubuntu.com",
"--receive-keys",
OP_PGP_KEYID,
)
def __del__(self):
shutil.rmtree(self.gnupghome)
def _run(self, *args):
try:
subprocess.run(args, env=self.env, check=True, capture_output=True)
except subprocess.CalledProcessError as cpe:
print(cpe.stderr, file=sys.stderr)
raise SystemExit(f"gpg error: {cpe.cmd}")
def verify(self, sigfile, datafile):
return self._run(self.gpgv, sigfile, datafile)
def nix_store_prefetch(url):
nix = shutil.which("nix")
cp = subprocess.run(
[nix, "store", "prefetch-file", "--json", url], check=True, capture_output=True
)
out = json.loads(cp.stdout)
return out["storePath"], out["hash"]
def mk_url(channel, os, version, arch):
if os == "linux":
arch_alias = {"x86_64": "x64", "aarch64": "arm64"}[arch]
path = f"linux/tar/{channel}/{arch}/1password-{version}.{arch_alias}.tar.gz"
elif os == "darwin":
path = f"mac/1Password-{version}-{arch}.zip"
else:
raise SystemExit(f"update-sources.py: unsupported OS {os}")
return f"{DOWNLOADS_BASE_URL}/{path}"
def download(channel, os, version, arch):
url = mk_url(channel, os, version, arch)
store_path_tarball, hash = nix_store_prefetch(url)
# Linux release tarballs come with detached PGP signatures.
if os == "linux":
store_path_sig, _ = nix_store_prefetch(url + ".sig")
GPG().verify(store_path_sig, store_path_tarball)
return url, hash
def main(args):
"""Gets called with args in `channel/os/version` format.
e.g.:
update-sources.py stable/linux/8.10.80 beta/linux/8.10.82-12.BETA
"""
sources = Sources()
for triplet in args[1:]:
channel, os, version = triplet.split("/")
release = sources[channel][os]
if release["version"] == version:
continue
release["version"] = version
for arch in release["sources"]:
url, hash = download(channel, os, version, arch)
release["sources"][arch].update({"url": url, "hash": hash})
sources.persist()
if __name__ == "__main__":
sys.exit(main(sys.argv))
+108 -74
View File
@@ -1,84 +1,118 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p jq gnupg
#!nix-shell -i bash -p jq curl
#shellcheck shell=bash
set -euo pipefail
# For Linux version checks we rely on Repology API to check 1Password managed Arch User Repository.
REPOLOGY_PROJECT_URI="https://repology.org/api/v1/project/1password"
# For Darwin version checks we query the same endpoint 1Password 8 for Mac queries.
# This is the base URI. For stable channel an additional path of "N", for beta channel, "Y" is required.
APP_UPDATES_URI_BASE="https://app-updates.agilebits.com/check/2/99/aarch64/OPM8/en/0/A1"
CURL=(
"curl" "--silent" "--show-error" "--fail"
"--proto" "=https" # enforce https
"--tlsv1.2" # do not accept anything below tls 1.2
"-H" "user-agent: nixpkgs#_1password-gui update.sh" # repology requires a descriptive user-agent
)
JQ=(
"jq"
"--raw-output"
"--exit-status" # exit non-zero if no output is produced
)
read_local_versions() {
local channel="$1"
while IFS='=' read -r key value; do
local_versions["${key}"]="${value}"
done < <(jq -r --arg channel "${channel}" '
.[$channel] | to_entries[] | .key as $os | .value.version as $version |
"\($channel)/\($os)=\($version)"
' sources.json)
}
read_remote_versions() {
local channel="$1"
if [[ ${channel} == "stable" ]]; then
remote_versions["stable/linux"]=$(
"${CURL[@]}" "${REPOLOGY_PROJECT_URI}" \
| "${JQ[@]}" '.[] | select(.repo == "aur" and .srcname == "1password" and .status == "newest") | .version'
)
remote_versions["stable/darwin"]=$(
"${CURL[@]}" "${APP_UPDATES_URI_BASE}/N" \
| "${JQ[@]}" 'select(.available == "1") | .version'
)
else
remote_versions["beta/linux"]=$(
# AUR version string uses underscores instead of dashes for betas.
# We fix that with a `sub` in jq query.
"${CURL[@]}" "${REPOLOGY_PROJECT_URI}" \
| "${JQ[@]}" '.[] | select(.repo == "aur" and .srcname == "1password-beta") | .version | sub("_"; "-")'
)
remote_versions["beta/darwin"]=$(
"${CURL[@]}" "${APP_UPDATES_URI_BASE}/Y" \
| "${JQ[@]}" 'select(.available == "1") | .version'
)
fi
}
render_versions_json() {
local key value
for key in "${!local_versions[@]}"; do
value="${local_versions[${key}]}"
echo "${key}"
echo "${value}"
done \
| jq -nR 'reduce inputs as $i ({}; . + { $i: input })'
}
cd -- "$(dirname "${BASH_SOURCE[0]}")"
mk_url() {
local \
base_url="https://downloads.1password.com" \
os="$1" \
channel="$2" \
arch="$3" \
version="$4"
attr_path=${UPDATE_NIX_ATTR_PATH}
case "${attr_path}" in
_1password-gui) channel="stable" ;;
_1password-gui-beta) channel="beta" ;;
*)
echo "Unknown attribute path ${attr_path}" >&2
exit 1
esac
if [[ ${os} == "linux" ]]; then
if [[ ${arch} == "x86_64" ]]; then
ext="x64.tar.gz"
else
ext="arm64.tar.gz"
fi
url="${base_url}/${os}/tar/${channel}/${arch}/1password-${version}.${ext}"
else
ext="${arch}.zip"
url="${base_url}/mac/1Password-${version}-${ext}"
declare -A local_versions remote_versions
declare -a new_version_available=()
read_local_versions "${channel}"
read_remote_versions "${channel}"
for i in "${!remote_versions[@]}"; do
if [[ "${local_versions[$i]}" != "${remote_versions[$i]}" ]]; then
old_version="${local_versions[$i]}"
new_version="${remote_versions[$i]}"
new_version_available+=("$i/$new_version")
fi
echo "${url}"
}
cleanup() {
if [[ -d ${TMP_GNUPGHOME-} ]]; then
rm -r "${TMP_GNUPGHOME}"
fi
if [[ -f ${JSON_HEAP-} ]]; then
rm "${JSON_HEAP}"
fi
}
trap cleanup EXIT
# Get channel versions from versions.json
declare -A versions
while IFS='=' read -r key value; do
versions["${key}"]="${value}"
done < <(jq -r 'to_entries[] | "\(.key)=\(.value)"' versions.json)
TMP_GNUPGHOME=$(mktemp -dt 1password-gui.gnupghome.XXXXXX)
export GNUPGHOME="${TMP_GNUPGHOME}"
gpg --no-default-keyring --keyring trustedkeys.kbx \
--keyserver keyserver.ubuntu.com \
--receive-keys 3FEF9748469ADBE15DA7CA80AC2D62742012EA22
JSON_HEAP=$(mktemp -t 1password-gui.jsonheap.XXXXXX)
for channel in stable beta; do
for os in linux darwin; do
for arch in x86_64 aarch64; do
version="${versions[${channel}-${os}]}"
url=$(mk_url ${os} ${channel} ${arch} ${version})
nix store prefetch-file --json "${url}" | jq "
{
\"${channel}\": {
\"${arch}-${os}\": {
\"url\": \"${url}\",
\"hash\": .hash,
\"storePath\": .storePath
}
}
}" >> "${JSON_HEAP}"
# For some reason 1Password PGP signs only Linux binaries.
if [[ ${os} == "linux" ]]; then
gpgv \
$(nix store prefetch-file --json "${url}.sig" | jq -r .storePath) \
$(jq -r --slurp ".[-1].[].[].storePath" "${JSON_HEAP}")
fi
done
done
done
# Combine heap of hash+url objects into a single JSON object.
jq --slurp 'reduce .[] as $x ({}; . * $x) | del (.[].[].storePath)' "${JSON_HEAP}" > sources.json
if [[ ${#new_version_available[@]} -eq 0 ]]; then
# up to date
exit
fi
./update-sources.py "${new_version_available[@]}"
cat <<EOF
[
{
"attrPath": "${attr_path}",
"oldVersion": "${old_version}",
"newVersion": "${new_version}",
"files": [
"$PWD/sources.json"
]
}
]
EOF
@@ -1,6 +0,0 @@
{
"stable-linux": "8.10.78",
"stable-darwin": "8.10.78",
"beta-linux":"8.10.80-18.BETA",
"beta-darwin": "8.10.80-18.BETA"
}