diff options
author | jopejoe1 <johannes@joens.email> | 2023-12-06 19:37:56 +0100 |
---|---|---|
committer | Jörg Thalheim <Mic92@users.noreply.github.com> | 2023-12-13 21:28:44 +0100 |
commit | c34ac183ca5b34670d421ab504b6412c15a0ac9a (patch) | |
tree | 345e1a7f78f05fcffb5017bb7c0d6656dbb35905 /pkgs/by-name/no | |
parent | 546c660ff673bb4e95af205fcbdade43a6e9abbe (diff) |
noto-fonts-monochrome-emoji: move out of noto-fonts file
Diffstat (limited to 'pkgs/by-name/no')
4 files changed, 273 insertions, 0 deletions
diff --git a/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.hashes.json b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.hashes.json new file mode 100644 index 0000000000000..e9ad1c327f4f9 --- /dev/null +++ b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.hashes.json @@ -0,0 +1,7 @@ +{ + "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob_10jwvS-FGJCMY.ttf": "sha256-B8XBpYycOYBjrhjlnyiz42YukIoOjGTd3NN3EY00NiQ=", + "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0jwvS-FGJCMY.ttf": "sha256-Zfwh9q2GrL5Dwp+J/8Ddd2IXCaUXpQ7dE3CqgCMMyPs=", + "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-Z0jwvS-FGJCMY.ttf": "sha256-/O5b2DzM8g97NAdJgIC/RsQ7E5P7USKq7TXyDuUE3WQ=", + "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob911TwvS-FGJCMY.ttf": "sha256-vrjB8GlhzWAe6jG/Srpy8R431VivNtWbCa5Uh4ATnmU=", + "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob9M1TwvS-FGJCMY.ttf": "sha256-EbnZt8h4Lcl0yJoOKmXlF1nfcP5hZv7n4cEQ10yBkcg=" +} diff --git a/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.json b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.json new file mode 100644 index 0000000000000..c729634b9e1e7 --- /dev/null +++ b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.json @@ -0,0 +1,30 @@ +{ + "kind": "webfonts#webfontList", + "items": [ + { + "family": "Noto Emoji", + "variants": [ + "300", + "regular", + "500", + "600", + "700" + ], + "subsets": [ + "emoji" + ], + "version": "v47", + "lastModified": "2023-09-27", + "files": { + "300": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob_10jwvS-FGJCMY.ttf", + "regular": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0jwvS-FGJCMY.ttf", + "500": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-Z0jwvS-FGJCMY.ttf", + "600": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob911TwvS-FGJCMY.ttf", + "700": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob9M1TwvS-FGJCMY.ttf" + }, + "category": "sans-serif", + "kind": "webfonts#webfont", + "menu": "http://fonts.gstatic.com/s/notoemoji/v47/bMrnmSyK7YY-MEu6aWjPDs-ar6uWaGWuob-r0gwuQeU.ttf" + } + ] +} diff --git a/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.py b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.py new file mode 100755 index 0000000000000..9f1eadd95bca5 --- /dev/null +++ b/pkgs/by-name/no/noto-fonts-monochrome-emoji/noto-emoji.py @@ -0,0 +1,183 @@ +#!/usr/bin/env nix-shell +#! nix-shell -i "python3 -I" -p python3 + +from contextlib import contextmanager +from pathlib import Path +from typing import Iterable, Optional +from urllib import request + +import hashlib, json + + +def getMetadata(apiKey: str, family: str = "Noto Emoji"): + '''Fetch the Google Fonts metadata for a given family. + + An API key can be obtained by anyone with a Google account (🚮) from + `https://developers.google.com/fonts/docs/developer_api#APIKey` + ''' + from urllib.parse import urlencode + + with request.urlopen( + "https://www.googleapis.com/webfonts/v1/webfonts?" + + urlencode({ 'key': apiKey, 'family': family }) + ) as req: + return json.load(req) + +def getUrls(metadata) -> Iterable[str]: + '''Fetch all files' URLs from Google Fonts' metadata. + + The metadata must obey the API v1 schema, and can be obtained from: + https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_TOKEN}&family=${FAMILY} + ''' + return ( url for i in metadata['items'] for _, url in i['files'].items() ) + + +def hashUrl(url: str, *, hash: str = 'sha256'): + '''Compute the hash of the data from HTTP GETing a given `url`. + + The `hash` must be an algorithm name `hashlib.new` accepts. + ''' + with request.urlopen(url) as req: + return hashlib.new(hash, req.read()) + + +def sriEncode(h) -> str: + '''Encode a hash in the SRI format. + + Takes a `hashlib` object, and produces a string that + nixpkgs' `fetchurl` accepts as `hash` parameter. + ''' + from base64 import b64encode + return f"{h.name}-{b64encode(h.digest()).decode()}" + +def validateSRI(sri: Optional[str]) -> Optional[str]: + '''Decode an SRI hash, return `None` if invalid. + + This is not a full SRI hash parser, hash options aren't supported. + ''' + from base64 import b64decode + + if sri is None: + return None + + try: + hashName, b64 = sri.split('-', 1) + + h = hashlib.new(hashName) + digest = b64decode(b64, validate=True) + assert len(digest) == h.digest_size + + except: + return None + else: + return sri + + +def hashUrls( + urls: Iterable[str], + knownHashes: dict[str, str] = {}, +) -> dict[str, str]: + '''Generate a `dict` mapping URLs to SRI-encoded hashes. + + The `knownHashes` optional parameter can be used to avoid + re-downloading files whose URL have not changed. + ''' + return { + url: validateSRI(knownHashes.get(url)) or sriEncode(hashUrl(url)) + for url in urls + } + + +@contextmanager +def atomicFileUpdate(target: Path): + '''Atomically replace the contents of a file. + + Yields an open file to write into; upon exiting the context, + the file is closed and (atomically) replaces the `target`. + + Guarantees that the `target` was either successfully overwritten + with new content and no exception was raised, or the temporary + file was cleaned up. + ''' + from tempfile import mkstemp + fd, _p = mkstemp( + dir = target.parent, + prefix = target.name, + ) + tmpPath = Path(_p) + + try: + with open(fd, 'w') as f: + yield f + + tmpPath.replace(target) + + except Exception: + tmpPath.unlink(missing_ok = True) + raise + + +if __name__ == "__main__": + from os import environ + from urllib.error import HTTPError + + environVar = 'GOOGLE_FONTS_TOKEN' + currentDir = Path(__file__).parent + metadataPath = currentDir / 'noto-emoji.json' + + try: + apiToken = environ[environVar] + metadata = getMetadata(apiToken) + + except (KeyError, HTTPError) as exn: + # No API key in the environment, or the query was rejected. + match exn: + case KeyError if exn.args[0] == environVar: + print(f"No '{environVar}' in the environment, " + "skipping metadata update") + + case HTTPError if exn.getcode() == 403: + print("Got HTTP 403 (Forbidden)") + if apiToken != '': + print("Your Google API key appears to be valid " + "but does not grant access to the fonts API.") + print("Aborting!") + raise SystemExit(1) + + case HTTPError if exn.getcode() == 400: + # Printing the supposed token should be fine, as this is + # what the API returns on invalid tokens. + print(f"Got HTTP 400 (Bad Request), is this really an API token: '{apiToken}' ?") + case _: + # Unknown error, let's bubble it up + raise + + # In that case just use the existing metadata + with metadataPath.open() as metadataFile: + metadata = json.load(metadataFile) + + lastModified = metadata["items"][0]["lastModified"]; + print(f"Using metadata from file, last modified {lastModified}") + + else: + # If metadata was successfully fetched, validate and persist it + lastModified = metadata["items"][0]["lastModified"]; + print(f"Fetched current metadata, last modified {lastModified}") + with atomicFileUpdate(metadataPath) as metadataFile: + json.dump(metadata, metadataFile, indent = 2) + metadataFile.write("\n") # Pacify nixpkgs' dumb editor config check + + hashPath = currentDir / 'noto-emoji.hashes.json' + try: + with hashPath.open() as hashFile: + hashes = json.load(hashFile) + except FileNotFoundError: + hashes = {} + + with atomicFileUpdate(hashPath) as hashFile: + json.dump( + hashUrls(getUrls(metadata), knownHashes = hashes), + hashFile, + indent = 2, + ) + hashFile.write("\n") # Pacify nixpkgs' dumb editor config check diff --git a/pkgs/by-name/no/noto-fonts-monochrome-emoji/package.nix b/pkgs/by-name/no/noto-fonts-monochrome-emoji/package.nix new file mode 100644 index 0000000000000..08f78b613efc7 --- /dev/null +++ b/pkgs/by-name/no/noto-fonts-monochrome-emoji/package.nix @@ -0,0 +1,53 @@ +{ lib +, stdenvNoCC +, fetchurl +}: + +# Metadata fetched from +# https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_TOKEN}&family=Noto+Emoji +let + metadata = with builtins; head (fromJSON (readFile ./noto-emoji.json)).items; + urlHashes = with builtins; fromJSON (readFile ./noto-emoji.hashes.json); +in +stdenvNoCC.mkDerivation { + pname = "noto-fonts-monochrome-emoji"; + version = "${lib.removePrefix "v" metadata.version}.${metadata.lastModified}"; + preferLocalBuild = true; + + dontUnpack = true; + srcs = + let + weightNames = { + "300" = "Light"; + regular = "Regular"; + "500" = "Medium"; + "600" = "SemiBold"; + "700" = "Bold"; + }; + in + lib.mapAttrsToList + (variant: url: fetchurl { + name = "NotoEmoji-${weightNames.${variant}}.ttf"; + hash = urlHashes.${url}; + inherit url; + }) + metadata.files; + + installPhase = '' + runHook preInstall + for src in $srcs; do + install -D $src $out/share/fonts/noto/$(stripHash $src) + done + runHook postInstall + ''; + + meta = { + description = "Monochrome emoji font"; + homepage = "https://fonts.google.com/noto/specimen/Noto+Emoji"; + license = [ lib.licenses.ofl ]; + maintainers = [ lib.maintainers.nicoo ]; + + platforms = lib.platforms.all; + sourceProvenance = [ lib.sourceTypes.binaryBytecode ]; + }; +} |