add nix package manager#442
Conversation
Balasubramanyamkosuri
left a comment
There was a problem hiding this comment.
Review summary
Solid NixCommand implementation — clear dispatch by native tool and appropriate netrc auth with cleanup.
Suggestions (non-blocking)
- Document which subcommands collect build-info vs passthrough (
nix-channelis passthrough only). - Add an IT or doc example for
nix copyURL format matching Artifactory Nix repo Set Me Up (api/nix/<repo>/). - Clear error when
nix/nix-env/nix-channelis not on PATH. - After build-info-go#378 merges (with mergeArtifacts fix), pin released build-info-go version in go.mod.
Blocked on: build-info-go#378 mergeArtifacts fix before final dependency bump.
Structured review: docs/reviews/nix-pr-review.md in team workspace.
| tmpFile, err := os.CreateTemp("", "nix-netrc-") | ||
| if err != nil { | ||
| return | ||
| } | ||
| if _, err = tmpFile.WriteString(netrcContent); err != nil { | ||
| return | ||
| } | ||
| if err = tmpFile.Close(); err != nil { | ||
| return | ||
| } | ||
| c.netrcPath = tmpFile.Name() |
There was a problem hiding this comment.
Two issues here:
- If WriteString (line 610) or Close (line 613) fails, the function
returns early but tmpFile is never closed and the temp file is never
deleted — OS fd + disk leak. Add_ = tmpFile.Close(); _ = os.Remove(tmpFile.Name())
in each early-return error path. - The function has no return value, so auth failures are invisible to
the caller. Callers in Run() (line 80) just continue silently and Nix
runs unauthenticated, producing cryptic "access denied" errors downstream.
Change the signature tocreateNetrcFile() errorand return fmt.Errorf
on each failure.
| req, err := http.NewRequest(http.MethodGet, url, nil) | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| if c.serverDetails.User != "" { | ||
| password := c.serverDetails.Password | ||
| if password == "" { | ||
| password = c.serverDetails.AccessToken | ||
| } | ||
| req.SetBasicAuth(c.serverDetails.User, password) | ||
| } | ||
|
|
||
| resp, err := client.Do(req) | ||
| if err != nil || resp.StatusCode != http.StatusOK { | ||
| if resp != nil { | ||
| _ = resp.Body.Close() | ||
| } | ||
| return "" | ||
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
|
|
||
| var repoConfig struct { | ||
| Rclass string `json:"rclass"` | ||
| DefaultDeploymentRepo string `json:"defaultDeploymentRepo"` | ||
| } | ||
| if err := json.NewDecoder(resp.Body).Decode(&repoConfig); err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| if repoConfig.Rclass == "virtual" && repoConfig.DefaultDeploymentRepo != "" { | ||
| return repoConfig.DefaultDeploymentRepo | ||
| } | ||
| return "" |
There was a problem hiding this comment.
This raw http.Client bypasses all the jfrog-client-go transport layer, which means:
- InsecureTls / custom CA certs in ServerDetails are ignored.
- Token-only auth is silently skipped — line 489 checks
c.serverDetails.User != ""
but modern JFrog Platform setups often use access tokens with no User field,
so no Authorization header is set at all.
Fix: if User is empty but AccessToken is set, use:
req.Header.Set("Authorization", "Bearer "+c.serverDetails.AccessToken) - Consider replacing this with the existing Artifactory service manager /
jfrog-client-go repository API to get retries and proper TLS handling for free.
| narXzName, narXzPath := c.findNarFile(searchRepo, dirPath) | ||
| if narXzName != "" { | ||
| artChecksum := c.getArtifactChecksums(searchRepo, narXzPath) | ||
| if artChecksum.Sha1 != "" || artChecksum.Sha256 != "" { | ||
| buildInfo.Modules[0].Dependencies[i].Checksum = artChecksum | ||
| resolved++ | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
findNarFile() (line 327/398) and getArtifactChecksums() (line 329/413)
each call utils.CreateServiceManager internally. For a Nix closure with
50–200 store paths this creates hundreds of ServiceManager instances and
HTTP connection pools in a tight loop.
Create a single ServiceManager once before the loop and thread it through,
or promote it to a field on NixCommand set during Run().
| var pkgAttr string | ||
| for i := len(c.args) - 1; i >= 0; i-- { | ||
| if !strings.HasPrefix(c.args[i], "-") && strings.Contains(c.args[i], ".") { | ||
| pkgAttr = c.args[i] | ||
| break | ||
| } | ||
| } | ||
| if pkgAttr == "" { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The dot-check on line 248 means nix-env -i hello silently skips
build-info collection with no warning. Package names without a channel
prefix (e.g. plain "hello", "git") are completely ignored.
At minimum add a log.Warn here so the user knows why no build-info
was collected. Ideally document in the method comment which invocation
patterns are supported.
| output, err := cmd.Output() | ||
| if err != nil { | ||
| return fmt.Errorf("nix-build failed: %s: %w", string(output), err) |
There was a problem hiding this comment.
cmd.Output() captures stdout; nix-build writes store paths to stdout,
not errors. Errors go to stderr which is already forwarded to os.Stderr
(line 140), so string(output) in the error message will be the store
path (or empty), not a diagnostic.
Use cmd.CombinedOutput() or just drop string(output) from the
fmt.Errorf call.
| func (c *NixCommand) setBuildPropertiesInDir(repo, dirPath, namePattern, buildName, buildNumber, timestamp string) { | ||
| if c.serverDetails == nil { | ||
| return | ||
| } | ||
| servicesManager, err := utils.CreateServiceManager(c.serverDetails, -1, 0, false) | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp) | ||
| searchQuery := fmt.Sprintf(`{"repo": "%s", "$or": [{"$and":[{"path": "%s","name": {"$match": "%s"}}]}]}`, repo, dirPath, namePattern) | ||
| searchParams := services.SearchParams{ | ||
| CommonParams: &specutils.CommonParams{ | ||
| Aql: specutils.Aql{ItemsFind: searchQuery}, | ||
| }, | ||
| } | ||
| reader, err := servicesManager.SearchFiles(searchParams) | ||
| if err != nil { | ||
| return | ||
| } | ||
| _, _ = servicesManager.SetProps(services.PropsParams{Reader: reader, Props: props}) | ||
| } |
There was a problem hiding this comment.
Every error path in this function returns without logging or surfacing
anything. A failed SetProps call is completely invisible to the user.
The function should at minimum log.Warn on each failure, and ideally
return error so the caller (tagUploadedArtifacts) can decide whether
to abort or continue.
| case "nix-channel": | ||
| return c.runNixChannel() | ||
| case "nix-env": | ||
| return c.runNixEnv() | ||
| case "nix-build": | ||
| return c.runNixBuild() | ||
| case "build": | ||
| return c.runNixFlakeBuild() | ||
| case "copy": | ||
| return c.runNixCopy() |
There was a problem hiding this comment.
do we actually need to capture all this commands? what about nixEnv, will capturing this modify our build-info?
| func (c *NixCommand) runNixChannel() error { | ||
| log.Info("Running nix-channel") | ||
| cmd := exec.Command("nix-channel", c.args...) | ||
| cmd.Env = c.buildEnv() | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| if err := cmd.Run(); err != nil { | ||
| return fmt.Errorf("nix-channel failed: %w", err) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
why can't this be a simple pass through?
| log.Info("Running nix-env") | ||
| cmd := exec.Command("nix-env", c.args...) | ||
| cmd.Env = c.buildEnv() | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| if err := cmd.Run(); err != nil { | ||
| return fmt.Errorf("nix-env failed: %w", err) | ||
| } |
There was a problem hiding this comment.
this line is being constantly repeated, consider making this a utility function or a mere pass through
| if c.repo != "" && c.serverDetails != nil { | ||
| deployRepo := c.resolveDefaultDeploymentRepo(c.repo) | ||
| if deployRepo != "" && deployRepo != c.repo { | ||
| log.Info(fmt.Sprintf("Resolved default deployment repo: %s → %s", c.repo, deployRepo)) | ||
| c.replaceRepoInToArg(c.repo, deployRepo) | ||
| c.repo = deployRepo | ||
| // Add --refresh so nix re-checks the LOCAL repo (which is empty) | ||
| // instead of using cached knowledge from previous virtual repo checks | ||
| c.args = append([]string{"--refresh"}, c.args...) | ||
| } | ||
| } |
There was a problem hiding this comment.
check if a similar utility method exists, also what happens if defaultdDeploymentRepo is not present
| return nil | ||
| } | ||
|
|
||
| // collectDepsFromEnvArgs resolves the store path from nix-env args (e.g., "nixpkgs.hello"). |
There was a problem hiding this comment.
why do we have so many different ways and logic across build-info-go and here to resolve storePaths?
| return entities.Checksum{} | ||
| } | ||
|
|
||
| func (c *NixCommand) getBuildNameAndNumber() (string, string, error) { |
There was a problem hiding this comment.
a common utility might also be present, also are we not collecting project info from flag?
| } | ||
|
|
||
| // getArtifactChecksums fetches sha1/sha256/md5 for a file in Artifactory. | ||
| func (c *NixCommand) getArtifactChecksums(repo, pathInRepo string) entities.Checksum { |
There was a problem hiding this comment.
why are we making a call per file basis? let's group them per repo and make a single shot call to get all the details
| } | ||
|
|
||
| props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp) | ||
| searchQuery := fmt.Sprintf(`{"repo": "%s", "$or": [{"$and":[{"path": "%s","name": {"$match": "%s"}}]}]}`, repo, dirPath, namePattern) |
There was a problem hiding this comment.
https://github.com/jfrog/jfrog-cli-artifactory/pull/442/changes#diff-9ef932742f174167bd4f2969755f537daa50c6562bacad7cdd71b66ee31701d9R653
findNarFiles function might already be giving you this info, consider caching it and reusing to reduce network calls
| } | ||
|
|
||
| // createNetrcFile creates a temporary netrc file for Nix authentication. | ||
| func (c *NixCommand) createNetrcFile() { |
There was a problem hiding this comment.
why are we doing this? are we not considering user's config files?
| timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) | ||
| var artifacts []entities.Artifact | ||
|
|
||
| for _, sp := range closurePaths { |
There was a problem hiding this comment.
let's try to combine them in a single network call, check for better efficiency
Added the support for nix package manager