···131131132132- [transfer-sh](https://github.com/dutchcoders/transfer.sh), a tool that supports easy and fast file sharing from the command-line. Available as [services.transfer-sh](#opt-services.transfer-sh.enable).
133133134134+- [FCast Receiver](https://fcast.org), an open-source alternative to Chromecast and AirPlay. Available as [programs.fcast-receiver](#opt-programs.fcast-receiver.enable).
135135+134136- [MollySocket](https://github.com/mollyim/mollysocket) which allows getting Signal notifications via UnifiedPush.
135137136138- [Suwayomi Server](https://github.com/Suwayomi/Suwayomi-Server), a free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org). Available as [services.suwayomi-server](#opt-services.suwayomi-server.enable).
137139138140- [ping_exporter](https://github.com/czerwonk/ping_exporter), a Prometheus exporter for ICMP echo requests. Available as [services.prometheus.exporters.ping](#opt-services.prometheus.exporters.ping.enable).
141141+142142+- [Prometheus DNSSEC Exporter](https://github.com/chrj/prometheus-dnssec-exporter), check for validity and expiration in DNSSEC signatures and expose metrics for Prometheus. Available as [services.prometheus.exporters.dnssec](#opt-services.prometheus.exporters.dnssec.enable).
139143140144- [TigerBeetle](https://tigerbeetle.com/), a distributed financial accounting database designed for mission critical safety and performance. Available as [services.tigerbeetle](#opt-services.tigerbeetle.enable).
141145···282286- `mkosi` was updated to v20. Parts of the user interface have changed. Consult the
283287 release notes of [v19](https://github.com/systemd/mkosi/releases/tag/v19) and
284288 [v20](https://github.com/systemd/mkosi/releases/tag/v20) for a list of changes.
289289+290290+- `gonic` has been updated to v0.16.4. Config now requires `playlists-path` to be set. See the rest of the [v0.16.0 release notes](https://github.com/sentriz/gonic/releases/tag/v0.16.0) for more details.
285291286292- The `services.vikunja` systemd service now uses `vikunja` as dynamic user instead of `vikunja-api`. Database users might need to be changed.
287293
+2-2
nixos/modules/config/shells-environment.nix
···4242 strings. The latter is concatenated, interspersed with colon
4343 characters.
4444 '';
4545- type = with types; attrsOf (oneOf [ (listOf str) str path ]);
4646- apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else "${v}");
4545+ type = with types; attrsOf (oneOf [ (listOf (oneOf [ float int str ])) float int str path ]);
4646+ apply = mapAttrs (n: v: if isList v then concatMapStringsSep ":" toString v else toString v);
4747 };
48484949 environment.profiles = mkOption {
···2727}:
28282929let
3030- version = "1.17.1";
3030+ version = "1.17.2";
31313232 # build stimuli file for PGO build and the script to generate it
3333 # independently of the foot's build, so we can cache the result
···9999 owner = "dnkl";
100100 repo = "foot";
101101 rev = version;
102102- hash = "sha256-B6RhzsOPwczPLJRx3gBFZZvklwx9IwqplRG2vsAPIlg=";
102102+ hash = "sha256-p+qaWHBrUn6YpNyAmQf6XoQyO3degHP5oMN53/9gIr4=";
103103 };
104104105105 separateDebugInfo = true;
···11-#!/usr/bin/env nix-shell
22-#! nix-shell -i "python3 -I" -p python3
33-44-from contextlib import contextmanager
55-from pathlib import Path
66-from typing import Iterable, Optional
77-from urllib import request
88-99-import hashlib, json
1010-1111-1212-def getMetadata(apiKey: str, family: str = "Noto Emoji"):
1313- '''Fetch the Google Fonts metadata for a given family.
1414-1515- An API key can be obtained by anyone with a Google account (🚮) from
1616- `https://developers.google.com/fonts/docs/developer_api#APIKey`
1717- '''
1818- from urllib.parse import urlencode
1919-2020- with request.urlopen(
2121- "https://www.googleapis.com/webfonts/v1/webfonts?" +
2222- urlencode({ 'key': apiKey, 'family': family })
2323- ) as req:
2424- return json.load(req)
2525-2626-def getUrls(metadata) -> Iterable[str]:
2727- '''Fetch all files' URLs from Google Fonts' metadata.
2828-2929- The metadata must obey the API v1 schema, and can be obtained from:
3030- https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_TOKEN}&family=${FAMILY}
3131- '''
3232- return ( url for i in metadata['items'] for _, url in i['files'].items() )
3333-3434-3535-def hashUrl(url: str, *, hash: str = 'sha256'):
3636- '''Compute the hash of the data from HTTP GETing a given `url`.
3737-3838- The `hash` must be an algorithm name `hashlib.new` accepts.
3939- '''
4040- with request.urlopen(url) as req:
4141- return hashlib.new(hash, req.read())
4242-4343-4444-def sriEncode(h) -> str:
4545- '''Encode a hash in the SRI format.
4646-4747- Takes a `hashlib` object, and produces a string that
4848- nixpkgs' `fetchurl` accepts as `hash` parameter.
4949- '''
5050- from base64 import b64encode
5151- return f"{h.name}-{b64encode(h.digest()).decode()}"
5252-5353-def validateSRI(sri: Optional[str]) -> Optional[str]:
5454- '''Decode an SRI hash, return `None` if invalid.
5555-5656- This is not a full SRI hash parser, hash options aren't supported.
5757- '''
5858- from base64 import b64decode
5959-6060- if sri is None:
6161- return None
6262-6363- try:
6464- hashName, b64 = sri.split('-', 1)
6565-6666- h = hashlib.new(hashName)
6767- digest = b64decode(b64, validate=True)
6868- assert len(digest) == h.digest_size
6969-7070- except:
7171- return None
7272- else:
7373- return sri
7474-7575-7676-def hashUrls(
7777- urls: Iterable[str],
7878- knownHashes: dict[str, str] = {},
7979-) -> dict[str, str]:
8080- '''Generate a `dict` mapping URLs to SRI-encoded hashes.
8181-8282- The `knownHashes` optional parameter can be used to avoid
8383- re-downloading files whose URL have not changed.
8484- '''
8585- return {
8686- url: validateSRI(knownHashes.get(url)) or sriEncode(hashUrl(url))
8787- for url in urls
8888- }
8989-9090-9191-@contextmanager
9292-def atomicFileUpdate(target: Path):
9393- '''Atomically replace the contents of a file.
9494-9595- Yields an open file to write into; upon exiting the context,
9696- the file is closed and (atomically) replaces the `target`.
9797-9898- Guarantees that the `target` was either successfully overwritten
9999- with new content and no exception was raised, or the temporary
100100- file was cleaned up.
101101- '''
102102- from tempfile import mkstemp
103103- fd, _p = mkstemp(
104104- dir = target.parent,
105105- prefix = target.name,
106106- )
107107- tmpPath = Path(_p)
108108-109109- try:
110110- with open(fd, 'w') as f:
111111- yield f
112112-113113- tmpPath.replace(target)
114114-115115- except Exception:
116116- tmpPath.unlink(missing_ok = True)
117117- raise
118118-119119-120120-if __name__ == "__main__":
121121- from os import environ
122122- from urllib.error import HTTPError
123123-124124- environVar = 'GOOGLE_FONTS_TOKEN'
125125- currentDir = Path(__file__).parent
126126- metadataPath = currentDir / 'noto-emoji.json'
127127-128128- try:
129129- apiToken = environ[environVar]
130130- metadata = getMetadata(apiToken)
131131-132132- except (KeyError, HTTPError) as exn:
133133- # No API key in the environment, or the query was rejected.
134134- match exn:
135135- case KeyError if exn.args[0] == environVar:
136136- print(f"No '{environVar}' in the environment, "
137137- "skipping metadata update")
138138-139139- case HTTPError if exn.getcode() == 403:
140140- print("Got HTTP 403 (Forbidden)")
141141- if apiToken != '':
142142- print("Your Google API key appears to be valid "
143143- "but does not grant access to the fonts API.")
144144- print("Aborting!")
145145- raise SystemExit(1)
146146-147147- case HTTPError if exn.getcode() == 400:
148148- # Printing the supposed token should be fine, as this is
149149- # what the API returns on invalid tokens.
150150- print(f"Got HTTP 400 (Bad Request), is this really an API token: '{apiToken}' ?")
151151- case _:
152152- # Unknown error, let's bubble it up
153153- raise
154154-155155- # In that case just use the existing metadata
156156- with metadataPath.open() as metadataFile:
157157- metadata = json.load(metadataFile)
158158-159159- lastModified = metadata["items"][0]["lastModified"];
160160- print(f"Using metadata from file, last modified {lastModified}")
161161-162162- else:
163163- # If metadata was successfully fetched, validate and persist it
164164- lastModified = metadata["items"][0]["lastModified"];
165165- print(f"Fetched current metadata, last modified {lastModified}")
166166- with atomicFileUpdate(metadataPath) as metadataFile:
167167- json.dump(metadata, metadataFile, indent = 2)
168168- metadataFile.write("\n") # Pacify nixpkgs' dumb editor config check
169169-170170- hashPath = currentDir / 'noto-emoji.hashes.json'
171171- try:
172172- with hashPath.open() as hashFile:
173173- hashes = json.load(hashFile)
174174- except FileNotFoundError:
175175- hashes = {}
176176-177177- with atomicFileUpdate(hashPath) as hashFile:
178178- json.dump(
179179- hashUrls(getUrls(metadata), knownHashes = hashes),
180180- hashFile,
181181- indent = 2,
182182- )
183183- hashFile.write("\n") # Pacify nixpkgs' dumb editor config check