diff --git a/.gitignore b/.gitignore index 8d3f6eb..1621d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /gonix +.direnv +result diff --git a/cmd/nix-vanity/main.go b/cmd/nix-vanity/main.go new file mode 100644 index 0000000..622db43 --- /dev/null +++ b/cmd/nix-vanity/main.go @@ -0,0 +1,325 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/nix-community/go-nix/pkg/derivation" + "github.com/schollz/progressbar/v3" + "golang.org/x/exp/slog" +) + +const ( + nixStorePrefix = "/nix/store/" + // omitted: E O U T + // https://github.com/NixOS/nix/blob/d12d69ea1a871d631d77c8ef5e8468b4a2bff80f/src/libutil/hash.cc#L73 + nixHashChars = "0123456789abcdfghijklmnpqrsvwxyz" + nixMaxHashLength = 32 +) + +// lookupDrvReplacementFromFileSystem remains largely the same, but ensure memoization is handled correctly. +// It's crucial that the memoize map is shared across all recursive calls initiated +// for a single top-level derivation's inputs. +func lookupDrvReplacementFromFileSystem(memoize map[string]string) func(string) (string, error) { + // This recursive function needs to capture the memoize map + var lookupFunc func(string) (string, error) + lookupFunc = func(drvPath string) (string, error) { + if memoized, found := memoize[drvPath]; found { + return memoized, nil + } + + f, err := os.Open(drvPath) + if err != nil { + // Wrap error for context + return "", fmt.Errorf("opening drv %q: %w", drvPath, err) + } + defer f.Close() + + drv, err := derivation.ReadDerivation(f) + if err != nil { + return "", fmt.Errorf("reading drv %q: %w", drvPath, err) + } + + // Pass the *same* lookupFunc (which captures the memoize map) recursively + replacement, err := drv.CalculateDrvReplacementRecursive(lookupFunc) + if err != nil { + return "", fmt.Errorf("calculating replacement for drv %q: %w", drvPath, err) + } + + // memoize the result + memoize[drvPath] = replacement + return replacement, nil + } + return lookupFunc +} + +// result holds the successful seed and the resulting derivation +type result struct { + seed string + drv *derivation.Derivation +} + +func main() { + // --- Configuration --- + var ( + derivationPath string + prefix string + numWorkers int + outputName string + seed uint64 + ) + + flag.Uint64Var(&seed, "seed", 0, "Initial seed for the random number generator (default: 0)") + flag.StringVar(&prefix, "prefix", "", "Desired prefix for the 'out' output path (e.g., /nix/store/abc)") + flag.IntVar(&numWorkers, "workers", runtime.NumCPU(), "Number of concurrent workers") + flag.StringVar(&outputName, "output", "out", "Name of the output path to check for the prefix") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options] \n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + if flag.NArg() != 1 { + slog.Error("Missing required argument: ") + flag.Usage() + os.Exit(1) + } + derivationPath = flag.Arg(0) + + if prefix == "" { + slog.Error("Missing required flag: -prefix") + flag.Usage() + os.Exit(1) + } + + if !strings.HasPrefix(prefix, nixStorePrefix) { + slog.Error("Prefix does not start with /nix/store/", "prefix", prefix) + os.Exit(1) + } + + // Extract the part after /nix/store/ which should be the hash prefix + hashPrefixPart := strings.TrimPrefix(prefix, nixStorePrefix) + + if strings.Contains(hashPrefixPart, "-") { + slog.Error("Prefix should not contain '-'", "prefix", prefix) + slog.Info("The prefix should only contain the desired starting characters of the hash itself (e.g., /nix/store/abc).") + flag.Usage() + os.Exit(1) + } + + if len(hashPrefixPart) > nixMaxHashLength { + slog.Error(fmt.Sprintf("Prefix cannot be longer than %d characters", nixMaxHashLength), "prefix", prefix, "length", len(hashPrefixPart)) + flag.Usage() + os.Exit(1) + } + + for _, r := range hashPrefixPart { + if !strings.ContainsRune(nixHashChars, r) { + slog.Error("Prefix contains invalid character", "char", string(r), "prefix", prefix) + slog.Info("Valid characters are: " + nixHashChars) + flag.Usage() + os.Exit(1) + } + } + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) // Use Info level for less noise + slog.SetDefault(logger) + + // --- Load Base Derivation --- + slog.Info("Loading base derivation", "path", derivationPath) + baseDrvFile, err := os.Open(derivationPath) + if err != nil { + slog.Error("Error opening base derivation file", "path", derivationPath, "error", err) + os.Exit(1) + } + defer baseDrvFile.Close() + + baseDrv, err := derivation.ReadDerivation(baseDrvFile) + if err != nil { + slog.Error("Error reading base derivation", "path", derivationPath, "error", err) + os.Exit(1) + } + + // Ensure Env is initialized + if baseDrv.Env == nil { + baseDrv.Env = make(map[string]string) + } + + // --- Calculate Input Derivation Replacements (Done Once) --- + slog.Info("Calculating input derivation replacements...") + // Use a single memoization map for all lookups related to the base derivation's inputs + inputMemoize := make(map[string]string, len(baseDrv.InputDerivations)*2) + lookupFunc := lookupDrvReplacementFromFileSystem(inputMemoize) + drvReplacements := make(map[string]string, len(baseDrv.InputDerivations)) + + for inputDrvPath := range baseDrv.InputDerivations { + // Note: We don't need to read the input drv file here again. + // CalculateDrvReplacementRecursive handles the recursion via the lookupFunc. + replacement, err := lookupFunc(inputDrvPath) + if err != nil { + slog.Error("Error calculating input replacement", "error", err) + os.Exit(1) + } + drvReplacements[inputDrvPath] = replacement + slog.Debug("Calculated replacement", "input", inputDrvPath, "replacement", replacement) + } + slog.Info("Finished calculating input derivation replacements.") + + // --- Setup Concurrent Search --- + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // Ensure cancellation signal is sent on exit + + var wg sync.WaitGroup + resultChan := make(chan result, 1) // Buffered channel to hold the first result + seedChan := make(chan uint64, numWorkers*2) // Channel to distribute seeds + var attempts atomic.Uint64 // Atomic counter for attempts + slog.Info("Starting workers", "count", numWorkers) + + // Progress Bar + // Use -1 for max to indicate unknown duration, updating manually + bar := progressbar.NewOptions64(-1, + progressbar.OptionSetDescription("Searching for prefix..."), + progressbar.OptionSetWriter(os.Stderr), + progressbar.OptionSetWidth(15), + progressbar.OptionThrottle(100*time.Millisecond), // Update interval + progressbar.OptionShowCount(), + progressbar.OptionShowTotalBytes(false), + progressbar.OptionSetItsString("drv"), + progressbar.OptionShowIts(), // Show iterations per second + progressbar.OptionSpinnerType(14), + progressbar.OptionFullWidth(), + progressbar.OptionSetRenderBlankState(true), + ) + + // --- Start Workers --- + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + slog.Debug("Worker started", "id", workerID) + + // Each worker needs its own *copy* of the environment map + // derived from the base derivation to avoid race conditions. + // We create a slightly modified derivation copy inside the loop. + + for { + select { + case <-ctx.Done(): // Check for cancellation signal + slog.Debug("Worker cancelling", "id", workerID) + return + case seed, ok := <-seedChan: + if !ok { + slog.Debug("Seed channel closed, worker stopping", "id", workerID) + return + } + + // Create a shallow copy of the base derivation for this attempt + currentDrv := *baseDrv + // Create a *new* environment map for this attempt + currentEnv := make(map[string]string, len(baseDrv.Env)+1) + for k, v := range baseDrv.Env { + currentEnv[k] = v + } + seedStr := strconv.FormatUint(seed, 10) + currentEnv["VANITY_SEED"] = seedStr + currentDrv.Env = currentEnv // Assign the unique env map + + // Calculate output paths using the modified derivation copy + outputs, err := currentDrv.CalculateOutputPaths(drvReplacements) + count := attempts.Add(1) // Increment attempt counter atomically + bar.Set64(int64(count)) // Update progress bar + + if err != nil { + // Log error but continue searching, might be transient? Or maybe stop? + // For now, log and continue. If this is common, might need rethinking. + slog.Warn("Error calculating output paths for seed", "seed", seedStr, "error", err) + continue // Try next seed + } + + // Check if the desired output path has the prefix + outputPath, found := outputs[outputName] + if !found { + // This should not happen if the derivation is valid, maybe exit? + slog.Error("Output name not found in calculated outputs", "output_name", outputName, "seed", seedStr) + // Optionally: cancel() here if this is critical + continue + } + + if strings.HasPrefix(outputPath, prefix) { + slog.Info("Prefix found!", "seed", seedStr, "output_name", outputName, "path", outputPath) + + // Prepare the final derivation object with the correct outputs + finalDrv := currentDrv // Start with the drv copy that worked + // Update Outputs map and Env map with ALL calculated outputs + for name, path := range outputs { + if _, ok := finalDrv.Outputs[name]; ok { + finalDrv.Outputs[name].Path = path + } else { + // This case might indicate an issue, but handle defensively + slog.Warn("Output name present in calculation but not in drv.Outputs map", "name", name) + // Decide if you want to add it or ignore + } + finalDrv.Env[name] = path // Ensure env var is also set + } + + // Try sending the result. If channel is full/closed, another worker won. + select { + case resultChan <- result{seed: seedStr, drv: &finalDrv}: + slog.Debug("Worker sent result", "id", workerID) + cancel() // Signal all other workers and seed generator to stop + case <-ctx.Done(): + // Context was cancelled while trying to send, another worker won. + slog.Debug("Context cancelled before worker could send result", "id", workerID) + } + return // This worker is done + } + // else: Prefix not matched, continue loop + } + } + }(i) + } + + // --- Start Seed Generator --- + go func() { + for { + select { + case <-ctx.Done(): // Stop generating if context is cancelled + slog.Debug("Seed generator stopping") + close(seedChan) // Close channel to signal workers no more seeds are coming + return + case seedChan <- seed: + seed++ + // Handle potential overflow if you run this for a *very* long time + if seed == 0 { // Check if wrapped around + slog.Warn("Seed counter overflowed!") + // Optionally stop or reset, depending on desired behavior + } + } + } + }() + + // --- Wait for Result or Completion --- + finalResult := <-resultChan + // Success! + bar.Finish() // Mark progress bar as complete + slog.Info("Successfully found seed", "seed", finalResult.seed) + + // Write the successful derivation to stdout + slog.Info("Writing successful derivation to stdout...") + if err := finalResult.drv.WriteDerivation(os.Stdout); err != nil { + slog.Error("Error writing final derivation", "error", err) + os.Exit(1) + } + + // Wait for all workers to finish cleanly after cancellation + wg.Wait() + slog.Info("All workers finished.") +} diff --git a/go.mod b/go.mod index 98dbe83..5574695 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,18 @@ module github.com/nix-community/go-nix -go 1.20 +go 1.23.0 + +toolchain go1.23.7 require ( github.com/adrg/xdg v0.5.0 github.com/alecthomas/kong v0.5.0 github.com/dgraph-io/badger/v3 v3.2103.2 github.com/mattn/go-sqlite3 v1.14.23 - github.com/multiformats/go-multihash v0.2.1 github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 + github.com/schollz/progressbar/v3 v3.18.0 github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 ) require ( @@ -25,17 +28,13 @@ require ( github.com/golang/snappy v0.0.3 // indirect github.com/google/flatbuffers v1.12.1 // indirect github.com/klauspost/compress v1.12.3 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect - github.com/multiformats/go-varint v0.0.6 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect go.opencensus.io v0.22.5 // indirect - golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.1.6 // indirect ) diff --git a/go.sum b/go.sum index 7724314..91228d7 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -52,27 +54,20 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108= -github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= -github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= -github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -80,7 +75,11 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -106,9 +105,9 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -138,8 +137,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -168,5 +169,3 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= -lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= diff --git a/pkg/derivation/hashes.go b/pkg/derivation/hashes.go index 6b69036..275c75d 100644 --- a/pkg/derivation/hashes.go +++ b/pkg/derivation/hashes.go @@ -141,6 +141,18 @@ func (d *Derivation) CalculateOutputPaths(inputDrvReplacements map[string]string // We solve this having calculateDrvReplacement accept a map of // /its/ replacements, instead of recursing. func (d *Derivation) CalculateDrvReplacement(inputDrvReplacements map[string]string) (string, error) { + return d.CalculateDrvReplacementRecursive(func(drvPath string) (string, error) { + replacement, ok := inputDrvReplacements[drvPath] + if !ok { + return "", fmt.Errorf("unable to find replacement for %s", drvPath) + } + + return replacement, nil + }) +} + +//nolint:lll +func (d *Derivation) CalculateDrvReplacementRecursive(lookupDrvReplacement func(drvPath string) (string, error)) (string, error) { // Check if we're a fixed output if len(d.Outputs) == 1 { // Is it fixed output? @@ -158,6 +170,18 @@ func (d *Derivation) CalculateDrvReplacement(inputDrvReplacements map[string]str h := sha256.New() + // Create a map of replacements by calling the provided function + inputDrvReplacements := make(map[string]string) + + for inputDrvPath := range d.InputDerivations { + replacement, err := lookupDrvReplacement(inputDrvPath) + if err != nil { + return "", fmt.Errorf("error getting replacement for %s: %w", inputDrvPath, err) + } + + inputDrvReplacements[inputDrvPath] = replacement + } + err := d.writeDerivation(h, false, inputDrvReplacements) if err != nil { return "", fmt.Errorf("error hashing ATerm: %w", err) diff --git a/pkg/derivation/hashes_test.go b/pkg/derivation/hashes_test.go new file mode 100644 index 0000000..0c90bc6 --- /dev/null +++ b/pkg/derivation/hashes_test.go @@ -0,0 +1,94 @@ +package derivation_test + +import ( + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "github.com/nix-community/go-nix/pkg/derivation" + "github.com/stretchr/testify/assert" +) + +func lookupDrvReplacement(drvPath string) (string, error) { + // strip the `/nix/store/` prefix + // lookup the file from ../../test/testdata/" + // call CalculateDrvReplacementRecursive on it + // and return the result + testPath := drvPath[len("/nix/store/"):] + testDataPath := filepath.Join("../../test/testdata/", testPath) + + f, err := os.Open(testDataPath) + if err != nil { + return "", err + } + defer f.Close() + + derivationBytes, err := io.ReadAll(f) + if err != nil { + return "", err + } + + drv, err := derivation.ReadDerivation(bytes.NewReader(derivationBytes)) + if err != nil { + panic(err) + } + + return drv.CalculateDrvReplacementRecursive(lookupDrvReplacement) +} + +func TestRecursiveLookup(t *testing.T) { + cases := []struct { + name string + path string + }{ + { + // derivation with no dependencies + name: "simple", + path: "8hx7v7vqgn8yssvpvb4zsjd6wbn7i9nn-simple.drv", + }, + { + // derivation with a single dependency (text file) + name: "simple with dependency", + path: "w0cji81iagsj1x6y34kn2lp9m3q00wj4-simple-with-dep.drv", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + drv := getDerivation(c.path) + _, err := drv.CalculateDrvReplacementRecursive(lookupDrvReplacement) + assert.NoError(t, err, "It should have found a replacement") + }) + } +} + +func TestCalculateOutputPathsRecursively(t *testing.T) { + drv := getDerivation("w0cji81iagsj1x6y34kn2lp9m3q00wj4-simple-with-dep.drv") + + // iterate over all inputs and calculate the drvReplacements for them + drvReplacements := make(map[string]string, len(drv.InputDerivations)) + + for inputdDrvPath := range drv.InputDerivations { + testPath := inputdDrvPath[len("/nix/store/"):] + inputDrv := getDerivation(testPath) + replacement, err := inputDrv.CalculateDrvReplacementRecursive(lookupDrvReplacement) + assert.NoError(t, err, "It should have found a replacement") + + drvReplacements[inputdDrvPath] = replacement + } + + outputs, err := drv.CalculateOutputPaths(drvReplacements) + assert.NoError(t, err, "It should have calculated outputs") + + // check if the output paths are correct + expectedOutputs := map[string]string{ + "out": "/nix/store/fz5klkd4sb99vrk6d33gh6fqsmfbkss1-simple-with-dep", + } + for outputName, expectedOutput := range expectedOutputs { + outputPath, ok := outputs[outputName] + assert.True(t, ok, "Output path should exist") + assert.Equal(t, expectedOutput, outputPath, "Output path should be correct") + } +} diff --git a/test/testdata/8hx7v7vqgn8yssvpvb4zsjd6wbn7i9nn-simple.drv b/test/testdata/8hx7v7vqgn8yssvpvb4zsjd6wbn7i9nn-simple.drv new file mode 100644 index 0000000..32b3912 --- /dev/null +++ b/test/testdata/8hx7v7vqgn8yssvpvb4zsjd6wbn7i9nn-simple.drv @@ -0,0 +1 @@ +Derive([("out","/nix/store/spw90kjdj9h0ry4zzizn7v1nn10mclll-simple","","")],[],[],"x86_64-linux","/bin/sh",["-c","echo hello world > $out"],[("builder","/bin/sh"),("name","simple"),("out","/nix/store/spw90kjdj9h0ry4zzizn7v1nn10mclll-simple"),("system","x86_64-linux")]) \ No newline at end of file diff --git a/test/testdata/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv b/test/testdata/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv new file mode 100644 index 0000000..29424d7 --- /dev/null +++ b/test/testdata/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv @@ -0,0 +1 @@ +Derive([("out","/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world","","")],[],[],"x86_64-linux","/bin/sh",["-c","echo hello world > $out"],[("builder","/bin/sh"),("name","hello-world"),("out","/nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world"),("system","x86_64-linux")]) \ No newline at end of file diff --git a/test/testdata/w0cji81iagsj1x6y34kn2lp9m3q00wj4-simple-with-dep.drv b/test/testdata/w0cji81iagsj1x6y34kn2lp9m3q00wj4-simple-with-dep.drv new file mode 100644 index 0000000..d6353d9 --- /dev/null +++ b/test/testdata/w0cji81iagsj1x6y34kn2lp9m3q00wj4-simple-with-dep.drv @@ -0,0 +1 @@ +Derive([("out","/nix/store/fz5klkd4sb99vrk6d33gh6fqsmfbkss1-simple-with-dep","","")],[("/nix/store/dp5na63mzhc3a2kkiy37b3xq642k7lhy-hello-world.drv",["out"])],[],"x86_64-linux","/bin/sh",["-c","cat /nix/store/kbh84yclhgn33j34j9i4wxha04v79q5f-hello-world > $out"],[("builder","/bin/sh"),("name","simple-with-dep"),("out","/nix/store/fz5klkd4sb99vrk6d33gh6fqsmfbkss1-simple-with-dep"),("system","x86_64-linux")]) \ No newline at end of file