diff --git a/pkgs/by-name/_1/_1password-gui/darwin.nix b/pkgs/by-name/_1/_1password-gui/darwin.nix index 66af74630c34..7c2f24f98be4 100644 --- a/pkgs/by-name/_1/_1password-gui/darwin.nix +++ b/pkgs/by-name/_1/_1password-gui/darwin.nix @@ -30,4 +30,6 @@ stdenv.mkDerivation { # 1Password is notarized. dontFixup = true; + + passthru.updateScript = ./update.sh; } diff --git a/pkgs/by-name/_1/_1password-gui/linux.nix b/pkgs/by-name/_1/_1password-gui/linux.nix index 9998860829a8..03fec5bd4922 100644 --- a/pkgs/by-name/_1/_1password-gui/linux.nix +++ b/pkgs/by-name/_1/_1password-gui/linux.nix @@ -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; } diff --git a/pkgs/by-name/_1/_1password-gui/package.nix b/pkgs/by-name/_1/_1password-gui/package.nix index e4a9866ff3f2..b1f68bf68035 100644 --- a/pkgs/by-name/_1/_1password-gui/package.nix +++ b/pkgs/by-name/_1/_1password-gui/package.nix @@ -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"; }; diff --git a/pkgs/by-name/_1/_1password-gui/sources.json b/pkgs/by-name/_1/_1password-gui/sources.json index ef4f2c8ee395..60b0a7e93b46 100644 --- a/pkgs/by-name/_1/_1password-gui/sources.json +++ b/pkgs/by-name/_1/_1password-gui/sources.json @@ -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=" + } + } } } } diff --git a/pkgs/by-name/_1/_1password-gui/update-sources.py b/pkgs/by-name/_1/_1password-gui/update-sources.py new file mode 100755 index 000000000000..f987d3fa3c5c --- /dev/null +++ b/pkgs/by-name/_1/_1password-gui/update-sources.py @@ -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)) diff --git a/pkgs/by-name/_1/_1password-gui/update.sh b/pkgs/by-name/_1/_1password-gui/update.sh index 1d31ef972f76..07fc726e2f74 100755 --- a/pkgs/by-name/_1/_1password-gui/update.sh +++ b/pkgs/by-name/_1/_1password-gui/update.sh @@ -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 <