Unverified Commit a3ecc9aa authored by lassulus's avatar lassulus Committed by GitHub
Browse files

Merge pull request #290052 from jopejoe1/noto/monochorme

noto-fonts-monochrome-emoji: refactor to use github
parents 0edd8f2a f6032a82
Loading
Loading
Loading
Loading
+0 −7
Original line number Diff line number Diff line
{
  "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="
}
+0 −30
Original line number Diff line number Diff line
{
  "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"
    }
  ]
}
+0 −183
Original line number Diff line number Diff line
#!/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
+17 −31
Original line number Diff line number Diff line
{ lib
, stdenvNoCC
, fetchurl
, fetchFromGitHub
, rename
}:

# 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";
  version = "3.000";

  src = fetchFromGitHub {
    owner = "google";
    repo = "fonts";
    rev = "a73b9ab0a5df191bcfed817159a903911ea7958a";
    hash = "sha256-qVFU4uZius8oFPJCIL9ek2YdS3jru5mmTHp2L9RIXfg=";
    sparseCheckout = [ "ofl/notoemoji" ];
  };
    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

    install -m444 -Dt $out/share/fonts/noto ofl/notoemoji/*.ttf
    ${rename}/bin/rename 's/\[.*\]//' $out/share/fonts/noto/*

    runHook postInstall
  '';

  passthru.updateScript = ./update.sh;

  meta = {
    description = "Monochrome emoji font";
    homepage = "https://fonts.google.com/noto/specimen/Noto+Emoji";
+25 −0
Original line number Diff line number Diff line
#!/usr/bin/env nix-shell
#! nix-shell -i bash -p common-updater-scripts git nix-prefetch

tmpdir=$(mktemp -d)

git -C "$tmpdir" init --initial-branch main
git -C "$tmpdir" config core.sparseCheckout true
git -C "$tmpdir" remote add origin https://github.com/google/fonts.git
echo "ofl/notoemoji/*" > "$tmpdir/.git/info/sparse-checkout"
git -C "$tmpdir" fetch origin main
git -C "$tmpdir" checkout main

newrev=$(git -C "$tmpdir" rev-list -1 HEAD "ofl/notoemoji/*.ttf")
newver=$(grep 'archive:' "$tmpdir/ofl/notoemoji/upstream.yaml" | grep -oP '(?<=v)[0-9]+\.[0-9]+')
newhash=$(nix-prefetch "{ stdenv, fetchurl }: stdenv.mkDerivation rec {
  name = \"noto-fonts-cjk-serif\";
  src = fetchFromGitHub {
    owner = \"google\";
    repo = \"fonts\";
    rev = \"$newrev\";
    sparseCheckout = [ \"ofl/notoemoji\" ];
  };
}")

update-source-version noto-fonts-monochrome-emoji "$newver" "$newhash" --rev="$newrev"