Skip to content

add nix package manager#442

Open
reshmifrog wants to merge 6 commits into
mainfrom
RTECO-1040-Nix-package-support
Open

add nix package manager#442
reshmifrog wants to merge 6 commits into
mainfrom
RTECO-1040-Nix-package-support

Conversation

@reshmifrog
Copy link
Copy Markdown
Contributor

  • All tests passed. If this feature is not already covered by the tests, I added new tests.
  • All static analysis checks passed.
  • Appropriate label is added to auto generate release notes.
  • I used gofmt for formatting the code before submitting the pull request.
  • PR description is clear and concise, and it includes the proposed solution/fix.

Added the support for nix package manager

@reshmifrog reshmifrog requested review from a team, agrasth, bhanurp, fluxxBot, itsmeleela and udaykb2 May 7, 2026 04:55
@reshmifrog reshmifrog added the new feature Automatically generated release notes label May 7, 2026
@reshmifrog reshmifrog added the safe to test Approve running integration tests on a pull request label May 7, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 7, 2026
Copy link
Copy Markdown

@Balasubramanyamkosuri Balasubramanyamkosuri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Solid NixCommand implementation — clear dispatch by native tool and appropriate netrc auth with cleanup.

Suggestions (non-blocking)

  1. Document which subcommands collect build-info vs passthrough (nix-channel is passthrough only).
  2. Add an IT or doc example for nix copy URL format matching Artifactory Nix repo Set Me Up (api/nix/<repo>/).
  3. Clear error when nix / nix-env / nix-channel is not on PATH.
  4. 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.

Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment thread artifactory/commands/nix/command.go
Comment on lines +606 to +616
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()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. 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.
  2. 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 to createNetrcFile() error and return fmt.Errorf
    on each failure.

Comment on lines +485 to +517
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 ""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raw http.Client bypasses all the jfrog-client-go transport layer, which means:

  1. InsecureTls / custom CA certs in ServerDetails are ignored.
  2. 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)
  3. Consider replacing this with the existing Artifactory service manager /
    jfrog-client-go repository API to get retries and proper TLS handling for free.

Comment on lines +327 to +335
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++
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Comment on lines +246 to +255
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
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +141 to +143
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("nix-build failed: %s: %w", string(output), err)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +629 to +650
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})
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +89 to +98
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()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we actually need to capture all this commands? what about nixEnv, will capturing this modify our build-info?

Comment on lines +105 to +115
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
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't this be a simple pass through?

Comment on lines +119 to +126
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)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is being constantly repeated, consider making this a utility function or a mere pass through

Comment on lines +197 to +207
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...)
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's try to combine them in a single network call, check for better efficiency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature Automatically generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants