From 85f741875c86e1bb758806a55233454039ce795f Mon Sep 17 00:00:00 2001 From: Niklas Zender Date: Tue, 23 Jun 2026 19:19:42 +0200 Subject: [PATCH] feat(dart): Add pinned Dart and Flutter SDK packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce centrally pinned Dart/Flutter SDK derivations so that the DevShell and CI can share identical toolchain binaries. - nix/dart/sdk-versions.nix: central version + SRI hash pins (generated) - nix/dart/packages/{dart,flutter}-sdk.nix: SDK derivations from the pins (flutter omitted on aarch64-linux — no upstream stable binary) - nix/dart/sdk.nix: expose packages.famedly-{dart,flutter}-sdk and the updateSdkVersions app, wired into nix/dart/default.nix - nix/dart/packages/update-sdk-versions.py: refresh pins from the official release-metadata endpoints; resolves the target file via the git repo root so it works under `nix run`, and emits output identical to the committed file to avoid drift - README: document how to update the SDK versions Co-authored-by: Cursor --- README.md | 24 +++ nix/dart/default.nix | 6 +- nix/dart/packages/dart-sdk.nix | 78 +++++++++ nix/dart/packages/flutter-sdk.nix | 103 ++++++++++++ nix/dart/packages/update-sdk-versions.py | 205 +++++++++++++++++++++++ nix/dart/sdk-versions.nix | 35 ++++ nix/dart/sdk.nix | 45 +++++ 7 files changed, 494 insertions(+), 2 deletions(-) create mode 100644 nix/dart/packages/dart-sdk.nix create mode 100644 nix/dart/packages/flutter-sdk.nix create mode 100755 nix/dart/packages/update-sdk-versions.py create mode 100644 nix/dart/sdk-versions.nix create mode 100644 nix/dart/sdk.nix diff --git a/README.md b/README.md index 4d9e945d..ae791d86 100644 --- a/README.md +++ b/README.md @@ -187,3 +187,27 @@ easy: More complex modifications to the `prek` wrapper are possible too, refer to our [wrapper module provider](https://birdeehub.github.io/nix-wrapper-modules/modules/default.html) for details. + +### Updating the Dart/Flutter SDK versions + +The Dart and Flutter SDKs are pinned centrally in +[`nix/dart/sdk-versions.nix`](nix/dart/sdk-versions.nix), so that the +DevShell and CI always use identical toolchain binaries. This file is +generated — do not edit it by hand. + +To bump the pins, run: + +```console +$ nix run .#updateSdkVersions # latest stable of both +$ nix run .#updateSdkVersions -- --dart X.Y.Z # pin a specific Dart version +$ nix run .#updateSdkVersions -- --flutter X.Y.Z # pin a specific Flutter version +``` + +This fetches the requested (or latest stable) versions, prefetches the +SDK archives for every supported platform, records their SHA256 hashes, +and rewrites `sdk-versions.nix`. Review and commit the result. + +The versions are resolved from the official release-metadata endpoints +that the Dart and Flutter tooling itself uses +(`dart-archive/.../latest/VERSION` and +`flutter_infra_release/releases/releases_linux.json`). diff --git a/nix/dart/default.nix b/nix/dart/default.nix index 2c35c02c..91820b5b 100644 --- a/nix/dart/default.nix +++ b/nix/dart/default.nix @@ -3,9 +3,11 @@ lib, importApply, ... -}: +}@args: importingFlake: { - imports = [ ]; + imports = [ + (importApply ./sdk.nix args) + ]; options.perSystem = flake-parts-lib.mkPerSystemOption ({ options.famedly.standards.dart.projects = lib.mkOption { diff --git a/nix/dart/packages/dart-sdk.nix b/nix/dart/packages/dart-sdk.nix new file mode 100644 index 00000000..0756c9dc --- /dev/null +++ b/nix/dart/packages/dart-sdk.nix @@ -0,0 +1,78 @@ +# Dart SDK derivation with a pinned version from nix/dart/sdk-versions.nix. +# Structurally identical to nixpkgs' dart/default.nix, but version and +# hashes come from our central pin so that DevShell and CI use the same binary. +{ + lib, + stdenv, + fetchurl, + unzip, + sdkVersions, +}: + +let + v = sdkVersions.dart; + inherit (stdenv.hostPlatform) system; + + platforms = { + x86_64-linux = "linux-x64"; + aarch64-linux = "linux-arm64"; + x86_64-darwin = "macos-x64"; + aarch64-darwin = "macos-arm64"; + }; + + archiveName = + platforms.${system} + or (throw "famedly-dart-sdk: unsupported system ${system}; supported: ${lib.concatStringsSep ", " (lib.attrNames platforms)}"); + + hash = v.hashes.${system} or (throw "famedly-dart-sdk: no hash for ${system}"); +in +stdenv.mkDerivation { + pname = "famedly-dart-sdk"; + inherit (v) version; + + src = fetchurl { + url = "https://storage.googleapis.com/dart-archive/channels/stable/release/${v.version}/sdk/dartsdk-${archiveName}-release.zip"; + inherit hash; + }; + + nativeBuildInputs = [ unzip ]; + + installPhase = '' + runHook preInstall + rm -f LICENSE README revision + cp -R . $out + runHook postInstall + ''; + + # Patch ELF interpreters on Linux so binaries work outside the Nix sandbox. + postInstall = lib.optionalString stdenv.hostPlatform.isLinux '' + find $out/bin -type f -executable | while read f; do + if patchelf --print-interpreter "$f" >/dev/null 2>&1; then + patchelf \ + --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ + --set-rpath "${lib.makeLibraryPath [ (lib.getLib stdenv.cc.cc) ]}" \ + "$f" + fi + done + ''; + + dontStrip = true; + + meta = { + description = "Dart SDK ${v.version} (Famedly-pinned)"; + homepage = "https://dart.dev"; + changelog = "https://github.com/dart-lang/sdk/blob/main/CHANGELOG.md"; + mainProgram = "dart"; + platforms = lib.attrNames platforms; + sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + license = lib.licenses.bsd3; + maintainers = [ + { + name = "Famedly GmbH"; + email = "info@famedly.com"; + github = "famedly"; + githubId = 46558835; + } + ]; + }; +} diff --git a/nix/dart/packages/flutter-sdk.nix b/nix/dart/packages/flutter-sdk.nix new file mode 100644 index 00000000..9fff691f --- /dev/null +++ b/nix/dart/packages/flutter-sdk.nix @@ -0,0 +1,103 @@ +# Flutter SDK derivation with a pinned version from nix/dart/sdk-versions.nix. +# Flutter stable does not publish Linux arm64 binaries — aarch64-linux is +# therefore not supported. +{ + lib, + stdenv, + fetchurl, + autoPatchelfHook, + bash, + curl, + git, + unzip, + xz, + fontconfig, + gtk3, + libGL, + libepoxy, + pango, + glib, + sdkVersions, +}: + +let + v = sdkVersions.flutter; + inherit (stdenv.hostPlatform) system; + + archives = { + x86_64-linux = { + url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${v.version}-stable.tar.xz"; + hash = v.hashes.x86_64-linux; + }; + x86_64-darwin = { + url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_${v.version}-stable.zip"; + hash = v.hashes.x86_64-darwin; + }; + aarch64-darwin = { + url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_${v.version}-stable.zip"; + hash = v.hashes.aarch64-darwin; + }; + }; + + archive = + archives.${system} + or (throw "famedly-flutter-sdk: unsupported system ${system}; supported: ${lib.concatStringsSep ", " (lib.attrNames archives)}"); +in +stdenv.mkDerivation { + pname = "famedly-flutter-sdk"; + inherit (v) version; + + src = fetchurl { inherit (archive) url hash; }; + + nativeBuildInputs = [ + unzip + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + autoPatchelfHook + xz + ]; + + buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ + bash + curl + git + fontconfig + gtk3 + libGL + libepoxy + pango + glib + ]; + + installPhase = '' + runHook preInstall + mkdir -p $out + cp -R . $out/flutter + # Symlink binaries for PATH + mkdir -p $out/bin + ln -s $out/flutter/bin/flutter $out/bin/flutter + ln -s $out/flutter/bin/dart $out/bin/dart + runHook postInstall + ''; + + dontStrip = true; + dontBuild = true; + + meta = { + description = "Flutter SDK ${v.version} (Famedly-pinned)"; + homepage = "https://flutter.dev"; + changelog = "https://github.com/flutter/flutter/blob/main/CHANGELOG.md"; + mainProgram = "flutter"; + platforms = lib.attrNames archives; + sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + license = lib.licenses.bsd3; + maintainers = [ + { + name = "Famedly GmbH"; + email = "info@famedly.com"; + github = "famedly"; + githubId = 46558835; + } + ]; + }; +} diff --git a/nix/dart/packages/update-sdk-versions.py b/nix/dart/packages/update-sdk-versions.py new file mode 100755 index 00000000..89a37b85 --- /dev/null +++ b/nix/dart/packages/update-sdk-versions.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Update nix/dart/sdk-versions.nix with the correct Dart and Flutter versions. +# +# Usage: +# nix run .#updateSdkVersions # latest stable of both +# nix run .#updateSdkVersions -- --dart X.Y.Z # pin a specific Dart +# nix run .#updateSdkVersions -- --flutter X.Y.Z # pin a specific Flutter +# +# Versions come from the authoritative, machine-readable release-metadata +# endpoints that the Dart and Flutter tooling itself uses: +# +# Dart: https://storage.googleapis.com/dart-archive/channels/stable/release/latest/VERSION +# -> {"date": ..., "version": "X.Y.Z", "revision": ...} +# Flutter: https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json +# -> current_release.stable -> matching releases[].version +# +# The script also prefetches the SDK archives for every supported platform +# and records their SHA256 SRI hashes, then rewrites sdk-versions.nix. + +import argparse +import json +import subprocess +import urllib.request +from pathlib import Path + +# Path of the pin file relative to the repository root. Resolved against +# `git rev-parse --show-toplevel` at runtime so it works both when run from +# a checkout and via `nix run` (where this script lives in the Nix store). +SDK_VERSIONS_RELPATH = "nix/dart/sdk-versions.nix" + + +def sdk_versions_file() -> Path: + repo_root = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + return Path(repo_root) / SDK_VERSIONS_RELPATH + +DART_VERSION_URL = ( + "https://storage.googleapis.com/dart-archive/channels/stable/release/latest/VERSION" +) +FLUTTER_RELEASES_URL = ( + "https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json" +) + +DART_PLATFORMS = { + "x86_64-linux": "linux-x64", + "aarch64-linux": "linux-arm64", + "x86_64-darwin": "macos-x64", + "aarch64-darwin": "macos-arm64", +} + +FLUTTER_ARCHIVES = { + "x86_64-linux": ("linux", "flutter_linux_{version}-stable.tar.xz"), + "x86_64-darwin": ("macos", "flutter_macos_{version}-stable.zip"), + "aarch64-darwin": ("macos", "flutter_macos_arm64_{version}-stable.zip"), +} + + +def fetch_json(url: str) -> dict: + print(f" Fetching {url}") + with urllib.request.urlopen(url, timeout=30) as r: + return json.load(r) + + +def prefetch_sri(url: str) -> str: + print(f" Prefetching {url}") + result = subprocess.run( + ["nix-prefetch-url", url, "--type", "sha256"], + capture_output=True, + text=True, + check=True, + ) + base32 = result.stdout.strip() + sri = subprocess.run( + ["nix", "hash", "convert", "--hash-algo", "sha256", "--to", "sri", base32], + capture_output=True, + text=True, + check=True, + ) + return sri.stdout.strip() + + +def get_dart_version() -> str: + return fetch_json(DART_VERSION_URL)["version"] + + +def get_flutter_version() -> str: + data = fetch_json(FLUTTER_RELEASES_URL) + current = data["current_release"]["stable"] + release = next(r for r in data["releases"] if r["hash"] == current) + return release["version"] + + +def dart_hashes(version: str) -> dict[str, str]: + result = {} + for nix_sys, archive_suffix in DART_PLATFORMS.items(): + url = ( + f"https://storage.googleapis.com/dart-archive/channels/stable/release" + f"/{version}/sdk/dartsdk-{archive_suffix}-release.zip" + ) + result[nix_sys] = prefetch_sri(url) + return result + + +def flutter_hashes(version: str) -> dict[str, str]: + result = {} + for nix_sys, (os_dir, archive_tpl) in FLUTTER_ARCHIVES.items(): + archive = archive_tpl.format(version=version) + url = ( + f"https://storage.googleapis.com/flutter_infra_release/releases" + f"/stable/{os_dir}/{archive}" + ) + result[nix_sys] = prefetch_sri(url) + return result + + +def render_nix(dart_ver: str, dart_h: dict, flutter_ver: str, flutter_h: dict) -> str: + def hashes_block(h: dict, indent: str = " ") -> str: + return "\n".join(f'{indent}{k} = "{v}";' for k, v in sorted(h.items())) + + return f"""\ +# Central SDK version pins for Dart and Flutter. +# +# These versions are consumed by the Dart/Flutter SDK derivations +# (packages/dart-sdk.nix, packages/flutter-sdk.nix) and are intended to +# guarantee identical SDKs locally (DevShell) and in CI. +# +# DO NOT edit this file by hand. Regenerate it instead: +# nix run .#updateSdkVersions # latest stable of both +# nix run .#updateSdkVersions -- --dart X.Y.Z # pin a specific version +# nix run .#updateSdkVersions -- --flutter X.Y.Z +# +# See packages/update-sdk-versions.py for the version sources. +# +# Hashes are SHA256 SRI (sha256-) for official upstream binaries. +{{ + dart = {{ + version = "{dart_ver}"; + hashes = {{ +{hashes_block(dart_h)} + }}; + }}; + + # Flutter stable does not publish Linux arm64 binaries. + flutter = {{ + version = "{flutter_ver}"; + hashes = {{ +{hashes_block(flutter_h)} + }}; + }}; +}} +""" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Update nix/dart/sdk-versions.nix with the correct Dart/Flutter versions." + ) + parser.add_argument( + "--dart", + metavar="X.Y.Z", + help="Pin a specific Dart version instead of the latest stable.", + ) + parser.add_argument( + "--flutter", + metavar="X.Y.Z", + help="Pin a specific Flutter version instead of the latest stable.", + ) + args = parser.parse_args() + + if args.dart: + dart_ver = args.dart + print(f"==> Using requested Dart version: {dart_ver}") + else: + print("==> Fetching latest Dart stable version...") + dart_ver = get_dart_version() + print(f" Dart: {dart_ver}") + + if args.flutter: + flutter_ver = args.flutter + print(f"==> Using requested Flutter version: {flutter_ver}") + else: + print("==> Fetching latest Flutter stable version...") + flutter_ver = get_flutter_version() + print(f" Flutter: {flutter_ver}") + + print("==> Prefetching Dart hashes...") + dart_h = dart_hashes(dart_ver) + + print("==> Prefetching Flutter hashes...") + flutter_h = flutter_hashes(flutter_ver) + + nix_content = render_nix(dart_ver, dart_h, flutter_ver, flutter_h) + target = sdk_versions_file() + target.write_text(nix_content) + print(f"\n==> Written {target}") + print(f" Dart {dart_ver}, Flutter {flutter_ver}") + print("\nNext: commit the updated sdk-versions.nix.") + + +if __name__ == "__main__": + main() diff --git a/nix/dart/sdk-versions.nix b/nix/dart/sdk-versions.nix new file mode 100644 index 00000000..7eb0b99f --- /dev/null +++ b/nix/dart/sdk-versions.nix @@ -0,0 +1,35 @@ +# Central SDK version pins for Dart and Flutter. +# +# These versions are consumed by the Dart/Flutter SDK derivations +# (packages/dart-sdk.nix, packages/flutter-sdk.nix) and are intended to +# guarantee identical SDKs locally (DevShell) and in CI. +# +# DO NOT edit this file by hand. Regenerate it instead: +# nix run .#updateSdkVersions # latest stable of both +# nix run .#updateSdkVersions -- --dart X.Y.Z # pin a specific version +# nix run .#updateSdkVersions -- --flutter X.Y.Z +# +# See packages/update-sdk-versions.py for the version sources. +# +# Hashes are SHA256 SRI (sha256-) for official upstream binaries. +{ + dart = { + version = "3.11.1"; + hashes = { + aarch64-darwin = "sha256-L/UXrBpAcA9Sv0MJ+2TA3Hqq4cTjiSe9uehoAo1rAvk="; + aarch64-linux = "sha256-F2RclAFLG0ahAOQTW2QjXPnBn5yaP7gUlZroKT816Yw="; + x86_64-darwin = "sha256-pLTOKT4LZtIysx/E9R9e35MOz1rAc7HvNnco8tH5jS0="; + x86_64-linux = "sha256-z/uPpK+3d8JjDGYxG/WesDTNPqDH+UrTJuGmLGqpwnI="; + }; + }; + + # Flutter stable does not publish Linux arm64 binaries. + flutter = { + version = "3.41.6"; + hashes = { + aarch64-darwin = "sha256-Faccw3Gr5tr7smf0P83ZtL4mxNXl2/SSg6efWNz5By0="; + x86_64-darwin = "sha256-BuyDNw06ESwn2QN0lD36v4jmVU8hsy5UPQFY7Mdvi68="; + x86_64-linux = "sha256-UDs+a301L8pdIbZHTsqVrVRNj8OwU3guq2OjYMf8dWk="; + }; + }; +} diff --git a/nix/dart/sdk.nix b/nix/dart/sdk.nix new file mode 100644 index 00000000..a680ff71 --- /dev/null +++ b/nix/dart/sdk.nix @@ -0,0 +1,45 @@ +{ ... }: +importingFlake: { + perSystem = + { + lib, + pkgs, + system, + ... + }: + let + sdkVersions = import ./sdk-versions.nix; + in + { + # SDK packages — the same binaries are intended to be used by the + # DevShell and CI workflows so that local and CI builds match. + # + # famedly-dart-sdk: all four supported platforms. + # famedly-flutter-sdk: not available on aarch64-linux (no upstream binary). + packages = { + famedly-dart-sdk = pkgs.callPackage ./packages/dart-sdk.nix { inherit sdkVersions; }; + } + // lib.optionalAttrs (lib.elem system [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]) { famedly-flutter-sdk = pkgs.callPackage ./packages/flutter-sdk.nix { inherit sdkVersions; }; }; + + apps.updateSdkVersions = { + type = "app"; + meta.description = "Update nix/dart/sdk-versions.nix to the latest stable Dart and Flutter releases"; + program = lib.getExe ( + pkgs.writeShellApplication { + name = "updateSdkVersions"; + runtimeInputs = [ + pkgs.nix + pkgs.python3 + ]; + text = '' + exec python3 ${./packages/update-sdk-versions.py} "$@" + ''; + } + ); + }; + }; +}