From 58d7d713e85b55b83da8c14b28f0152bdb712fa9 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 18 Aug 2021 19:49:32 -0400 Subject: [PATCH] initial experiments Signed-off-by: Christine Dodrill --- .envrc | 1 + doc.go | 6 ++ go.mod | 3 + maybedoer.go | 63 ++++++++++++++ nix/go-toolchain-rev.patch | 42 +++++++++ nix/go.nix | 12 +++ nix/sources.json | 38 ++++++++ nix/sources.nix | 174 +++++++++++++++++++++++++++++++++++++ shell.nix | 27 ++++++ 9 files changed, 366 insertions(+) create mode 100644 .envrc create mode 100644 doc.go create mode 100644 go.mod create mode 100644 maybedoer.go create mode 100644 nix/go-toolchain-rev.patch create mode 100644 nix/go.nix create mode 100644 nix/sources.json create mode 100644 nix/sources.nix create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..051d09d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +eval "$(lorri direnv)" diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..8da0d97 --- /dev/null +++ b/doc.go @@ -0,0 +1,6 @@ +// Package gonads is a collection of monadic constructs lifted out of the Haskell and Rust +// standard libraries for use with Go 2 generics. +// +// Please do not use this library in production. This package is Deprecated and should not +// be used in production. +package gonads diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6ce3a65 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module tulpa.dev/cadey/gonads + +go 1.18 diff --git a/maybedoer.go b/maybedoer.go new file mode 100644 index 0000000..f382e3a --- /dev/null +++ b/maybedoer.go @@ -0,0 +1,63 @@ +package gonads + +import "errors" + +var ( + ErrOptionIsNone = errors.New("gonads: Option value contains nothing") +) + +// Option is a container that might contain a value. +type Option[T any] struct { + val *T +} + +// IsSome returns true if the option contains a value. +func (o Option[T]) IsSome() bool { + return o.val != nil +} + +// IsNone returns false if the option does not contain a value. +func (o Option[T]) IsNone() bool { + return !o.IsSome() +} + +// Take safely fetches the value from the Option. +func (o Option[T]) Take() (*T, error) { + if o.IsNone() { + return nil, ErrOptionIsNone + } + + return o.val, nil +} + +// Set assigns a value to an Option. +func (o *Option[T]) Set(val *T) { + o.val = val +} + +func NewOption[T any](val *T) Option[T] { + return Option[T]{val: val} +} + +// Thunk represents an uncomputed value that is cached for faster use later. +type Thunk[T any] struct { + o Option[T] + doer func() *T +} + +// Force either returns the cached thunk value or computes it and caches the result. +func (t Thunk[T]) Force() *T { + if t.o.IsSome() { + return t.o.val + } + + t.o.Set(t.doer()) + return t.o.val +} + +func NewThunk[T any](doer func() *T) Thunk[T] { + return Thunk[T]{ + o: NewOption[T](nil), + doer: doer, + } +} diff --git a/nix/go-toolchain-rev.patch b/nix/go-toolchain-rev.patch new file mode 100644 index 0000000..76a4c64 --- /dev/null +++ b/nix/go-toolchain-rev.patch @@ -0,0 +1,42 @@ +From 9988e3646e0f301602b2d0f56e6527f370ddccce Mon Sep 17 00:00:00 2001 +From: Christine Dodrill +Date: Mon, 16 Aug 2021 19:58:24 -0400 +Subject: [PATCH] cmd/dist: support embedding of toolchain rev by envvar + +Git checkouts are not byte-for-byte reproducible and the exact hash of +them can drift as git's moods change. This patch enables users of +deterministic build systems such as Nix or Guix to build an exact +revision of Go from the git repo directly by feeding the proper hash +into the build system with the envvar GOLANG_TOOLCHAIN_REV. + +This should only be used as a last resort (such as when the source +directory is immutable, like in the nix-build context). + +This is a port of +https://github.com/tailscale/go/commit/6785c6aa7b55f795ece47b2ee775cd3feb58b29e. +--- + src/cmd/dist/build.go | 11 +++++++++++ + 1 file changed, 11 insertions(+) + +diff --git a/src/cmd/dist/build.go b/src/cmd/dist/build.go +index bec17696f304..70ca59dddb3f 100644 +--- a/src/cmd/dist/build.go ++++ b/src/cmd/dist/build.go +@@ -343,6 +343,17 @@ func branchtag(branch string) (tag string, precise bool) { + + // findgoversion determines the Go version to use in the version string. + func findgoversion() string { ++ // If the magic envvar `GOLANG_TOOLCHAIN_REV` is set, use that git ++ // revision. Git checkouts are not reproducible. This allows users ++ // to build a compiler reproducibly from a context by feeding the ++ // appropriate hash to the build system. ++ if rev := os.Getenv("GOLANG_TOOLCHAIN_REV"); rev != "" { ++ if len(rev) > 10 { ++ rev = rev[:10] ++ } ++ return rev ++ } ++ + // The $GOROOT/VERSION file takes priority, for distributions + // without the source repo. + path := pathf("%s/VERSION", goroot) diff --git a/nix/go.nix b/nix/go.nix new file mode 100644 index 0000000..c9638dd --- /dev/null +++ b/nix/go.nix @@ -0,0 +1,12 @@ +{ sources ? import ./sources.nix, pkgs ? import sources.nixpkgs { } }: + +let go = sources.go; + +in pkgs.go.overrideAttrs (attrs: rec { + version = go.rev; + src = go; + nativeBuildInputs = attrs.nativeBuildInputs ++ [ pkgs.git ]; + checkPhase = ""; + GOLANG_TOOLCHAIN_REV = go.rev; + patches = [ ./go-toolchain-rev.patch ]; +}) diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..6485b18 --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,38 @@ +{ + "go": { + "branch": "master", + "description": "The Go programming language", + "homepage": "https://golang.org", + "owner": "golang", + "repo": "go", + "rev": "8ff16c19909e5aecf51c6b993cba36ea51791f34", + "sha256": "1c8nv1hxia3iaya937b4asxiiny797sd8zj648jrha3nhkldal2k", + "type": "tarball", + "url": "https://github.com/golang/go/archive/8ff16c19909e5aecf51c6b993cba36ea51791f34.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "niv": { + "branch": "master", + "description": "Easy dependency management for Nix projects", + "homepage": "https://github.com/nmattia/niv", + "owner": "nmattia", + "repo": "niv", + "rev": "e0ca65c81a2d7a4d82a189f1e23a48d59ad42070", + "sha256": "1pq9nh1d8nn3xvbdny8fafzw87mj7gsmp6pxkdl65w2g18rmcmzx", + "type": "tarball", + "url": "https://github.com/nmattia/niv/archive/e0ca65c81a2d7a4d82a189f1e23a48d59ad42070.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs": { + "branch": "nixos-unstable", + "description": "Nix Packages collection", + "homepage": "", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e41ba38114055832e5ba4a851e9c00149eef3e4a", + "sha256": "01qh41a912vk6fsdh3w6wsl45ml0lbqlc9akpbw2hasjf6wwg3kn", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/e41ba38114055832e5ba4a851e9c00149eef3e4a.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000..1938409 --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,174 @@ +# This file has been generated by Niv. + +let + + # + # The fetchers. fetch_ fetches specs of type . + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + if spec ? ref then spec.ref else + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; + in + builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; }; + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import {} + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else {}; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs ( + name: spec: + if builtins.hasAttr "outPath" spec + then abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..c8e5056 --- /dev/null +++ b/shell.nix @@ -0,0 +1,27 @@ +{ ... }: + +let + sources = import ./nix/sources.nix; + pkgs = import sources.nixpkgs { + overlays = [ + (self: super: rec { + go = import ./nix/go.nix { }; + buildGoModule = pkgs.callPackage + "${sources.nixpkgs}/pkgs/development/go-modules/generic" { + inherit go; + }; + gopls = super.gopls.override { inherit buildGoModule; }; + goimports = super.goimports.override { inherit buildGoModule; }; + }) + ]; + }; +in pkgs.mkShell { + buildInputs = with pkgs; [ + go + gopls + goimports + + # keep this line if you use bash + pkgs.bashInteractive + ]; +}