aboutsummaryrefslogtreecommitdiffstats
path: root/modules/web/chromium/extensions/lib.nix
blob: c7aefe37618e36baf1ac53eee86af4f60267685e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
{
  lib,
  pkgs,
  config,
  checkForUpdates ? true,
}:
rec {
  # --- Hit the GitHub releases API and return the latest tag name. ---
  # Impure: requires --impure since there's no fixed output hash for the API
  # response itself. Optionally uses $GITHUB_TOKEN to dodge rate limits.
  fetchLatestGithubReleaseTag =
    { owner, repo }:
    let
      raw = builtins.fetchurl {
        url = "https://api.github.com/repos/${owner}/${repo}/releases/latest";
        name = "${repo}-latest-release.json";
      };
      json = builtins.fromJSON (builtins.readFile raw);
    in
    json.tag_name;

  # --- Compare pinned version against upstream latest, abort with instructions if stale. ---
  # Returns `version` unchanged on success so it can be threaded into the
  # derivation below and force this check to actually run.
  checkExtensionVersion =
    {
      pname,
      owner,
      repo,
      version,
      urlTemplate, # human-readable template shown in the error message
      tagPrefix ? "", # e.g. "v" if tags look like "v1.2.3"
    }:
    if !checkForUpdates then
      version
    else
      let
        latestTag = fetchLatestGithubReleaseTag { inherit owner repo; };
        latestVersion =
          if lib.hasPrefix tagPrefix latestTag then lib.removePrefix tagPrefix latestTag else latestTag;
      in
      if latestVersion != version then
        throw ''
          [${pname}] A newer release is available upstream — refusing to build a stale extension.

            pinned version : ${version}
            latest version : ${latestVersion}  (tag: ${latestTag})

          To upgrade, edit extensions/${pname}.nix:
            1. Set   version = "${latestVersion}";
            2. Point the url at the new release asset:
                 ${urlTemplate}
            3. Set   hash = lib.fakeHash;
               then re-run your switch — it'll fail with a hash mismatch showing
               the real sha256. Paste that in as the final hash.
            4. Re-run once more. This check passes once pinned == latest.

          To skip this check for now (e.g. offline / pure eval), set:
            web.chromium.checkForUpdates = false;
        ''
      else
        version;

  # --- Download + unpack a zip *or* crx into a plain unpacked-extension dir. ---
  # A CRX3 file is just: "Cr24" magic (4B) + version (4B) + header length N (4B)
  # + N bytes of protobuf header + a normal zip payload. We slice off the
  # header when isCrx = true, then unzip exactly like any other release zip —
  # so every extension, crx or not, goes through one identical pipeline.
  fetchUnpackedExtension =
    {
      pname,
      version,
      url,
      hash,
      isCrx ? false,
    }:
    pkgs.stdenv.mkDerivation {
      inherit pname version;
      src = pkgs.fetchurl { inherit url hash; };

      nativeBuildInputs = [
        config.file-management.ouch.package
      ];
      dontUnpack = true;

      buildPhase = ''
        runHook preBuild
        mkdir -p $out

        if [ "${lib.boolToString isCrx}" = "true" ]; then
          # Verify "Cr24" magic header
          magic=$(head -c 4 "$src")
          if [ "$magic" != "Cr24" ]; then
            echo "Error: $src is not a valid CRX file" >&2
            exit 1
          fi

          # Extract header length (bytes 8-11, little-endian)
          # -An (no address), -j8 (skip 8 bytes), -N4 (read 4 bytes), -tu1 (unsigned decimal 1-byte)
          bytes=$(od -An -j8 -N4 -tu1 "$src")

          # Read into individual variables and calculate the length
          read b1 b2 b3 b4 <<< $bytes
          hlen=$(( b1 + (b2 << 8) + (b3 << 16) + (b4 << 24) ))
          offset=$(( 12 + hlen ))

          dd if=$src of=payload.zip bs=1 skip=$offset status=none
          ouch decompress payload.zip --dir $out
        else
          ouch decompress $src --dir $out
        fi

        # Flatten a single wrapping folder (common in GitHub release zips)
        if [ "$(ls -1 $out | wc -l)" -eq 1 ] && [ -d "$out"/* ]; then
          shopt -s dotglob
          mv "$out"/*/* "$out"/ 2>/dev/null || true
          rmdir "$out"/*/ 2>/dev/null || true
          shopt -u dotglob
        fi

        runHook postBuild
      '';

      installPhase = "true";
    };
}