diff --git a/buildtools/cli.go b/buildtools/cli.go index fb74931d7..8b5a91964 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" conancommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/conan" + nixcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/nix" "io/fs" "os" "os/exec" @@ -32,8 +33,8 @@ import ( huggingfaceCommands "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/huggingface" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/mvn" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/npm" - containerutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/pnpm" + containerutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ocicontainer" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/terraform" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/yarn" commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" @@ -53,7 +54,7 @@ import ( "github.com/jfrog/jfrog-cli/docs/artifactory/terraformconfig" twinedocs "github.com/jfrog/jfrog-cli/docs/artifactory/twine" "github.com/jfrog/jfrog-cli/docs/buildtools/conan" - "github.com/jfrog/jfrog-cli/docs/buildtools/conanconfig" + "github.com/jfrog/jfrog-cli/docs/buildtools/nix" "github.com/jfrog/jfrog-cli/docs/buildtools/docker" dotnetdocs "github.com/jfrog/jfrog-cli/docs/buildtools/dotnet" "github.com/jfrog/jfrog-cli/docs/buildtools/dotnetconfig" @@ -69,17 +70,16 @@ import ( "github.com/jfrog/jfrog-cli/docs/buildtools/mvnconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/npmcommand" "github.com/jfrog/jfrog-cli/docs/buildtools/npmconfig" + "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmcommand" nugetdocs "github.com/jfrog/jfrog-cli/docs/buildtools/nuget" "github.com/jfrog/jfrog-cli/docs/buildtools/nugetconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/pipconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/pipenvconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/pipenvinstall" "github.com/jfrog/jfrog-cli/docs/buildtools/pipinstall" - "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmcommand" "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/poetry" "github.com/jfrog/jfrog-cli/docs/buildtools/poetryconfig" - uvcommand "github.com/jfrog/jfrog-cli/docs/buildtools/uvcommand" yarndocs "github.com/jfrog/jfrog-cli/docs/buildtools/yarn" "github.com/jfrog/jfrog-cli/docs/buildtools/yarnconfig" "github.com/jfrog/jfrog-cli/docs/common" @@ -358,19 +358,6 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: PoetryCmd, }, - { - Name: "uv", - Flags: cliutils.GetCommandFlags(cliutils.Uv), - Usage: uvcommand.GetDescription(), - HelpName: corecommon.CreateUsage("uv", uvcommand.GetDescription(), uvcommand.Usage), - UsageText: uvcommand.GetArguments(), - ArgsUsage: common.CreateEnvVars(), - SkipFlagParsing: true, - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: UvCmd, - Hidden: true, - }, { Name: "helm", Flags: cliutils.GetCommandFlags(cliutils.Helm), @@ -384,19 +371,6 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: HelmCmd, }, - { - Name: "conan-config", - Flags: cliutils.GetCommandFlags(cliutils.ConanConfig), - Aliases: []string{"conanc"}, - Usage: conanconfig.GetDescription(), - HelpName: corecommon.CreateUsage("conan-config", conanconfig.GetDescription(), conanconfig.Usage), - ArgsUsage: common.CreateEnvVars(), - BashComplete: corecommon.CreateBashCompletionFunc(), - Category: buildToolsCategory, - Action: func(c *cli.Context) error { - return cliutils.CreateConfigCmd(c, project.Conan) - }, - }, { Name: "conan", Hidden: false, @@ -410,6 +384,19 @@ func GetCommands() []cli.Command { Category: buildToolsCategory, Action: ConanCmd, }, + { + Name: "nix", + Hidden: false, + Flags: cliutils.GetCommandFlags(cliutils.Nix), + Usage: nix.GetDescription(), + HelpName: corecommon.CreateUsage("nix", nix.GetDescription(), nix.Usage), + UsageText: nix.GetArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: NixCmd, + }, { Name: "ruby-config", Flags: cliutils.GetCommandFlags(cliutils.RubyConfig), @@ -707,12 +694,9 @@ func MvnCmd(c *cli.Context) (err error) { if !xrayScan && format != "" { return cliutils.PrintHelpAndReturnError("The --format option can be sent only with the --scan option", c) } - scanOutputFormat := outputFormat.Table - if format != "" { - scanOutputFormat, err = outputFormat.ParseOutputFormat(format, outputFormat.All) - if err != nil { - return err - } + scanOutputFormat, err := outputFormat.GetOutputFormat(format) + if err != nil { + return err } mvnCmd := mvn.NewMvnCommand().SetConfiguration(buildConfiguration).SetConfigPath(configFilePath).SetGoals(filteredMavenArgs).SetThreads(threads).SetInsecureTls(insecureTls).SetDetailedSummary(detailedSummary || printDeploymentView).SetXrayScan(xrayScan).SetScanOutputFormat(scanOutputFormat) err = commands.Exec(mvnCmd) @@ -816,14 +800,9 @@ func GradleCmd(c *cli.Context) (err error) { if !xrayScan && format != "" { return cliutils.PrintHelpAndReturnError("The --format option can be sent only with the --scan option", c) } - var scanOutputFormat outputFormat.OutputFormat - if format == "" { - scanOutputFormat = outputFormat.Table - } else { - scanOutputFormat, err = outputFormat.ParseOutputFormat(format, outputFormat.All) - if err != nil { - return err - } + scanOutputFormat, err := outputFormat.GetOutputFormat(format) + if err != nil { + return err } printDeploymentView := log.IsStdErrTerminal() gradleCmd := gradle.NewGradleCommand().SetConfiguration(buildConfiguration).SetTasks(filteredGradleArgs).SetConfigPath(configFilePath).SetThreads(threads).SetDetailedSummary(detailedSummary || printDeploymentView).SetXrayScan(xrayScan).SetScanOutputFormat(scanOutputFormat) @@ -1080,52 +1059,6 @@ func goCmdVerification(c *cli.Context) (string, error) { return configFilePath, nil } -// containerManagerEnvVar lets users force the container manager used by 'jf docker' -// subcommands, bypassing auto-detection. Accepted values (case-insensitive): "docker", "podman". -const containerManagerEnvVar = "JFROG_CLI_CONTAINER_MANAGER" - -// podmanDetector is indirected through a package-level variable so tests can -// replace the real 'docker version' probe with a deterministic stub. -var podmanDetector = dockerIsPodman - -// resolveContainerManagerType returns the container manager to use when running 'jf docker' subcommands. -// -// Resolution order: -// 1. Explicit override via the JFROG_CLI_CONTAINER_MANAGER env var ("docker" or "podman"). -// 2. Auto-detection: if the local 'docker' binary reports Podman in its version output -// (i.e. the podman-docker shim or native podman aliased as docker), treat it as Podman -// so 'jf docker ...' works transparently for Podman users without daemon-socket access. -// 3. Default: Docker. -// -// Detection is intentionally conservative: only a positive "Podman" signal from 'docker version' -// switches behavior. Real Docker installations are unaffected. -func resolveContainerManagerType() containerutils.ContainerManagerType { - switch strings.ToLower(strings.TrimSpace(os.Getenv(containerManagerEnvVar))) { - case "podman": - log.Debug(containerManagerEnvVar + "=podman. Routing 'jf docker' subcommands through Podman.") - return containerutils.Podman - case "docker": - log.Debug(containerManagerEnvVar + "=docker. Routing 'jf docker' subcommands through Docker.") - return containerutils.DockerClient - } - if podmanDetector() { - log.Debug("Detected Podman-backed 'docker' CLI. Routing 'jf docker' subcommands through Podman.") - return containerutils.Podman - } - return containerutils.DockerClient -} - -// dockerIsPodman returns true if the local 'docker' binary is actually Podman -// (either via the podman-docker shim or an alias). Any error or missing binary returns false. -func dockerIsPodman() bool { - cmd := exec.Command("docker", "version") - out, err := cmd.CombinedOutput() - if err != nil { - return false - } - return strings.Contains(strings.ToLower(string(out)), "podman") -} - func dockerCmd(c *cli.Context) error { args := cliutils.ExtractCommand(c) var cmd, cmdArg string @@ -1177,7 +1110,7 @@ func pullCmd(c *cli.Context, image string) error { if err != nil { return err } - PullCommand := container.NewPullCommand(resolveContainerManagerType()) + PullCommand := container.NewPullCommand(containerutils.DockerClient) PullCommand.SetCmdParams(filteredDockerArgs).SetSkipLogin(skipLogin).SetImageTag(image).SetServerDetails(rtDetails).SetBuildConfiguration(buildConfiguration) supported, err := PullCommand.IsGetRepoSupported() if err != nil { @@ -1201,7 +1134,7 @@ func pushCmd(c *cli.Context, image string) (err error) { return } printDeploymentView := log.IsStdErrTerminal() - pushCommand := container.NewPushCommand(resolveContainerManagerType()) + pushCommand := container.NewPushCommand(containerutils.DockerClient) pushCommand.SetThreads(threads).SetDetailedSummary(detailedSummary || printDeploymentView).SetCmdParams(filteredDockerArgs).SetSkipLogin(skipLogin).SetBuildConfiguration(buildConfiguration).SetServerDetails(rtDetails).SetValidateSha(validateSha).SetImageTag(image) supported, err := pushCommand.IsGetRepoSupported() if err != nil { @@ -1524,7 +1457,7 @@ func dockerNativeCmd(c *cli.Context) error { if err != nil { return err } - cm := containerutils.NewManager(resolveContainerManagerType()) + cm := containerutils.NewManager(containerutils.DockerClient) return cm.RunNativeCmd(cleanArgs) } @@ -1831,53 +1764,6 @@ func PoetryCmd(c *cli.Context) error { return pythonCmd(c, project.Poetry) } -func UvCmd(c *cli.Context) error { - if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { - return err - } - if c.NArg() < 1 { - return cliutils.WrongNumberOfArgumentsHandler(c) - } - args := cliutils.ExtractCommand(c) - filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) - if err != nil { - return err - } - var serverID string - filteredArgs, serverID, err = coreutils.ExtractServerIdFromCommand(filteredArgs) - if err != nil { - return fmt.Errorf("failed to extract server ID: %w", err) - } - cmdName, uvArgs := getCommandName(filteredArgs) - // Peek at --publish-url to populate DeployerRepo for build-info enrichment. - // The flag is NOT consumed here — it is forwarded to uv as-is. - deployerRepo := "" - for i, arg := range uvArgs { - if strings.HasPrefix(arg, "--publish-url=") { - deployerRepo = strings.TrimPrefix(arg, "--publish-url=") - } else if arg == "--publish-url" && i+1 < len(uvArgs) { - deployerRepo = uvArgs[i+1] - } - } - uvCommand := python.NewNativeUVCommand(). - SetCommandName(cmdName). - SetArgs(uvArgs). - SetServerID(serverID). - SetDeployerRepo(deployerRepo). - SetBuildConfiguration(buildConfiguration) - // For help requests, bypass commands.Exec() to skip the concurrent usage-reporting - // goroutine that would otherwise make Artifactory version calls unnecessarily. - for _, a := range uvArgs { - if a == "-h" || a == "--help" { - return uvCommand.Run() - } - } - if cmdName == "help" || cmdName == "" { - return uvCommand.Run() - } - return commands.Exec(uvCommand) -} - // HelmCmd executes Helm commands with build info collection support func HelmCmd(c *cli.Context) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { @@ -2007,6 +1893,65 @@ func ConanCmd(c *cli.Context) error { return commands.Exec(conanCommand) } +func NixCmd(c *cli.Context) error { + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + + args := cliutils.ExtractCommand(c) + + // Extract build flags (--build-name, --build-number, --module, --project) before passing to Nix + filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) + if err != nil { + return err + } + + // Extract --repo and --server-id flags before passing to nix + var repo, serverID string + var cleanedArgs []string + for i := 0; i < len(filteredArgs); i++ { + arg := filteredArgs[i] + if strings.HasPrefix(arg, "--repo=") { + repo = strings.TrimPrefix(arg, "--repo=") + } else if arg == "--repo" && i+1 < len(filteredArgs) { + repo = filteredArgs[i+1] + i++ + } else if strings.HasPrefix(arg, "--server-id=") { + serverID = strings.TrimPrefix(arg, "--server-id=") + } else if arg == "--server-id" && i+1 < len(filteredArgs) { + serverID = filteredArgs[i+1] + i++ + } else { + cleanedArgs = append(cleanedArgs, arg) + } + } + filteredArgs = cleanedArgs + + cmdName, nixArgs := getCommandName(filteredArgs) + + // Use jfrog-cli-artifactory Nix command with build info support + cmd := nixcommand.NewNixCommand().SetCommandName(cmdName).SetArgs(nixArgs).SetBuildConfiguration(buildConfiguration) + if repo != "" { + cmd.SetRepo(repo) + } + + // Pass server details — use specific server if --server-id provided, else default + var serverDetails *coreConfig.ServerDetails + if serverID != "" { + serverDetails, err = coreConfig.GetSpecificConfig(serverID, true, true) + } else { + serverDetails, err = coreConfig.GetDefaultServerConf() + } + if err == nil && serverDetails != nil { + cmd.SetServerDetails(serverDetails) + } + + return commands.Exec(cmd) +} + func pythonCmd(c *cli.Context, projectType project.ProjectType) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err diff --git a/go.mod b/go.mod index 78e4089af..5d1c34ba2 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260405065840-c930d515ef34 - github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260429074430-a5871f2898b5 + github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260507055916-1a22779f8183 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430125911-ad12ac6f1316 github.com/jfrog/jfrog-cli-evidence v0.9.2 github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260430094150-ce7d9b371c6f @@ -248,6 +248,10 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260507045358-c25254577a25 + +replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260507043136-ec39c1c5ff9e + // replace github.com/jfrog/jfrog-cli-artifactory => github.com/agrasth/jfrog-cli-artifactory v0.2.2-0.20260428100316-aa702370ec20 // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.13.1-0.20260428071432-1e9d9a1991ad diff --git a/go.sum b/go.sum index a227a9f55..de61de994 100644 --- a/go.sum +++ b/go.sum @@ -406,8 +406,8 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 h1:EH0h86KwGvNHWyEBQoHoU9WfMMKy1GJ6jJQNmfy6E0U= -github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/jfrog/build-info-go v1.13.1-0.20260507043136-ec39c1c5ff9e h1:4EEOa1EFVmqGEXtXaXINvwr3fohBPEdU04J+tMaw6Nw= +github.com/jfrog/build-info-go v1.13.1-0.20260507043136-ec39c1c5ff9e/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= github.com/jfrog/froggit-go v1.21.1 h1:I/XUOO6GQ1d/rmBlM361F8T654C3ohIWrpw23xNL9JY= github.com/jfrog/froggit-go v1.21.1/go.mod h1:umBiakJB0CSPFfe0AHVaC3n9xsmUT7NGkDCny3bRchI= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= @@ -418,10 +418,8 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260405065840-c930d515ef34 h1:qD53oDmaw7+5HjaU7FupqbB55saabNzMoMtu3kJfmg4= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260405065840-c930d515ef34/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260429074430-a5871f2898b5 h1:+52DDmdSZFP1dxgeu0pkB1sQuoHa0PWbW7HVdFOqK3A= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260429074430-a5871f2898b5/go.mod h1:BV+aCTQsaZeFec2WjgmQjqlxecju4CkkM9NqfiFyjo0= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430091103-6242ecf15d29 h1:J5+08rOpv/avgt53jNFZ+j5gU8mllcj7Dcfja5Ewodw= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430091103-6242ecf15d29/go.mod h1:bjAkVD8c2W+jg4whqy10bSXDC/c+Se8/ll/GPp5F/+0= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260507045358-c25254577a25 h1:fJ67ikaYGaINyE4mM/dA8URv9IyAiuF9znVQ1CtOjjM= +github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260507045358-c25254577a25/go.mod h1:9l8+WjlIb9bn+HA4eRFD6GTU/Fzy+2O97VM/AvHHg1Y= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430125911-ad12ac6f1316 h1:xAl5D+SjLeRH1gCsSHFPpXJeQQBv2HDGqDTDkFOKJ2s= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430125911-ad12ac6f1316/go.mod h1:bjAkVD8c2W+jg4whqy10bSXDC/c+Se8/ll/GPp5F/+0= github.com/jfrog/jfrog-cli-evidence v0.9.2 h1:huiBzQSI9z3OF3l2RphthdXl1aH9zBsvAt+zLsApORI= diff --git a/nix_test.go b/nix_test.go new file mode 100644 index 000000000..e97352d39 --- /dev/null +++ b/nix_test.go @@ -0,0 +1,515 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + buildinfo "github.com/jfrog/build-info-go/entities" + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" + + "github.com/jfrog/jfrog-cli/inttestutils" + "github.com/jfrog/jfrog-cli/utils/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ==================== Initialization ==================== + +func initNixTest(t *testing.T) { + if !*tests.TestNix { + t.Skip("Skipping Nix test. To run Nix test add the '-test.nix=true' option.") + } + require.True(t, isRepoExist(tests.NixRemoteRepo), "Nix test remote repository doesn't exist.") + require.True(t, isRepoExist(tests.NixVirtualRepo), "Nix test virtual repository doesn't exist.") +} + +// ==================== Project Helpers ==================== + +func createNixProject(t *testing.T, outputFolder, projectName string) (string, func()) { + projectSrc := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "nix", projectName) + tmpDir, cleanupCallback := coretests.CreateTempDirWithCallbackAndAssert(t) + + projectPath := filepath.Join(tmpDir, outputFolder) + assert.NoError(t, biutils.CopyDir(projectSrc, projectPath, true, nil)) + return projectPath, cleanupCallback +} + +// ==================== FlexPack Install Tests ==================== + +func TestNixFlakeLockFlexPack(t *testing.T) { + testNixFlexPack(t, "flake lock") +} + +func testNixFlexPack(t *testing.T, nixSubcmd string) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + buildNumber := "1" + projectPath, cleanupProject := createNixProject(t, "nix-"+nixSubcmd, "nixproject") + defer cleanupProject() + + // Build args — split compound subcommands like "flake lock" into separate args + args := []string{"nix"} + args = append(args, strings.Split(nixSubcmd, " ")...) + args = append(args, "--build-name="+tests.NixBuildName, "--build-number="+buildNumber) + + testNixCmd(t, projectPath, buildNumber, filepath.Base(projectPath), 3, args) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.NixBuildName, artHttpDetails) +} + +func testNixCmd(t *testing.T, projectPath, buildNumber, module string, expectedDependencies int, args []string) { + wd, err := os.Getwd() + assert.NoError(t, err, "Failed to get current directory") + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec(args...)) + + // Validate local build-info was created with Nix module type + inttestutils.ValidateGeneratedBuildInfoModule(t, tests.NixBuildName, buildNumber, "", []string{module}, buildinfo.Nix) + + // Publish build-info + assert.NoError(t, artifactoryCli.Exec("bp", tests.NixBuildName, buildNumber)) + + // Get and validate published build-info + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, tests.NixBuildName, buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + + buildInfoModules := publishedBuildInfo.BuildInfo.Modules + require.Len(t, buildInfoModules, 1) + assert.Equal(t, module, buildInfoModules[0].Id) + assert.Equal(t, buildinfo.Nix, buildInfoModules[0].Type) + assert.Len(t, buildInfoModules[0].Dependencies, expectedDependencies) + + // Validate Nix-specific: narHash checksums in SRI format + for _, dep := range buildInfoModules[0].Dependencies { + assert.NotEmpty(t, dep.Checksum.Sha256, "SHA256 (narHash) should be present for dep %s", dep.Id) + assert.Contains(t, dep.Checksum.Sha256, "sha256-", + "narHash should be in SRI format for dep %s, got: %s", dep.Id, dep.Checksum.Sha256) + } + + // Validate scopes + for _, dep := range buildInfoModules[0].Dependencies { + assert.Equal(t, []string{"build"}, dep.Scopes, + "dep %s should have scope [build]", dep.Id) + } + + // Validate requestedBy is present + hasRequestedBy := false + for _, dep := range buildInfoModules[0].Dependencies { + if len(dep.RequestedBy) > 0 { + hasRequestedBy = true + break + } + } + assert.True(t, hasRequestedBy, "at least one dependency should have RequestedBy") +} + +// ==================== Build Info Flag Combination Tests ==================== + +func TestNixBuildInfoBothFlags(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-both-flags", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + buildName := "nix-both-flags-test" + buildNumber := "42" + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber)) + + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build-info should be found when both flags set") + require.Len(t, publishedBuildInfo.BuildInfo.Modules, 1) + assert.Greater(t, len(publishedBuildInfo.BuildInfo.Modules[0].Dependencies), 0) + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +func TestNixBuildInfoBuildNameOnly(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-name-only", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // Only --build-name, no --build-number → nix command runs, build-info may not be collected + err = jfrogCli.Exec("nix", "flake", "lock", "--build-name=nix-name-only-test") + // Command may succeed or fail depending on build-number extraction — just ensure no panic + _ = err +} + +func TestNixBuildInfoNoFlags(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-no-flags", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock")) +} + +// ==================== Module Override Test ==================== + +func TestNixModuleOverride(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-module-override", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + buildName := "nix-module-override-test" + buildNumber := "1" + customModule := "my-custom-nix-module" + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber, + "--module="+customModule)) + + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + + if found && len(publishedBuildInfo.BuildInfo.Modules) > 0 { + assert.Equal(t, customModule, publishedBuildInfo.BuildInfo.Modules[0].Id) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +// ==================== Table-Driven Subcommand Tests ==================== + +func TestNixSubcommandVariants(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + allTests := []struct { + name string + nixSubcmd string + expectedDependencies int + collectsBuildInfo bool + }{ + {"nix-flake-lock", "flake lock", 3, true}, + } + + for buildNumber, test := range allTests { + t.Run(test.name, func(t *testing.T) { + buildNumberStr := strconv.Itoa(buildNumber + 1) + projectPath, cleanupProject := createNixProject(t, test.name, "nixproject") + defer cleanupProject() + + if test.collectsBuildInfo { + args := []string{"nix"} + args = append(args, strings.Split(test.nixSubcmd, " ")...) + args = append(args, "--build-name="+tests.NixBuildName, "--build-number="+buildNumberStr) + testNixCmd(t, projectPath, buildNumberStr, filepath.Base(projectPath), + test.expectedDependencies, args) + } + }) + + if test.collectsBuildInfo { + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, tests.NixBuildName, artHttpDetails) + } + } +} + +// ==================== Multiple Builds Test ==================== + +func TestNixMultipleBuilds(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + buildName := "nix-multi-build-test" + + for i := 1; i <= 3; i++ { + buildNumber := strconv.Itoa(i) + projectPath, cleanupProject := createNixProject(t, fmt.Sprintf("nix-multi-%d", i), "nixproject") + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber)) + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build %s should be found", buildNumber) + assert.Len(t, publishedBuildInfo.BuildInfo.Modules, 1) + + chdirCallback() + cleanupProject() + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +// ==================== Dependency Checksums Test ==================== + +func TestNixDependencyChecksums(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-checksums", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + buildName := "nix-checksum-test" + buildNumber := "1" + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber)) + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found) + + depsWithChecksums := 0 + for _, dep := range publishedBuildInfo.BuildInfo.Modules[0].Dependencies { + if dep.Checksum.Sha256 != "" { + depsWithChecksums++ + // Verify SRI format + assert.Contains(t, dep.Checksum.Sha256, "sha256-", + "dep %s should have narHash in SRI format", dep.Id) + } + } + assert.Greater(t, depsWithChecksums, 0, "at least one dep should have checksums") + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +// ==================== Project Key Test ==================== + +func TestNixFlakeLockWithProjectKey(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + projectPath, cleanupProject := createNixProject(t, "nix-project-key", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + buildName := "nix-project-key-test" + buildNumber := "1" + + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber, + "--project=testprj")) + + // Note: build-publish with --project may fail if project doesn't exist on this Artifactory instance. + // This is expected — the test validates that the --project flag is correctly passed through. + err = artifactoryCli.Exec("bp", buildName, buildNumber, "--project=testprj") + if err != nil { + t.Logf("build-publish with --project failed (project may not exist): %v", err) + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} + +// ==================== Round-Trip Test ==================== + +func TestNixRoundTrip(t *testing.T) { + initNixTest(t) + + setEnvCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallback() + + oldHomeDir, newHomeDir := prepareHomeDir(t) + defer func() { + clientTestUtils.SetEnvAndAssert(t, coreutils.HomeDir, oldHomeDir) + clientTestUtils.RemoveAllAndAssert(t, newHomeDir) + }() + + buildName := "nix-round-trip-test" + buildNumber := "1" + projectPath, cleanupProject := createNixProject(t, "nix-round-trip", "nixproject") + defer cleanupProject() + + wd, err := os.Getwd() + assert.NoError(t, err) + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, projectPath) + defer chdirCallback() + + // Step 1: Run nix flake lock with build-info + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("nix", "flake", "lock", + "--build-name="+buildName, "--build-number="+buildNumber)) + + // Step 2: Publish build-info + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + // Step 3: Retrieve and validate full round-trip + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + assert.NoError(t, err) + assert.True(t, found, "build-info should be found after round-trip") + + bi := publishedBuildInfo.BuildInfo + assert.Equal(t, buildName, bi.Name) + assert.Equal(t, buildNumber, bi.Number) + require.Len(t, bi.Modules, 1) + + module := bi.Modules[0] + assert.Equal(t, buildinfo.Nix, module.Type) + assert.Len(t, module.Dependencies, 3, "should have 3 deps: nixpkgs, flake-utils, systems") + + depIDs := make(map[string]bool) + for _, dep := range module.Dependencies { + depIDs[dep.Id] = true + assert.Contains(t, dep.Checksum.Sha256, "sha256-") + assert.Equal(t, []string{"build"}, dep.Scopes) + assert.Contains(t, dep.Id, ":") + } + + assert.True(t, depIDs["nixpkgs:0ad13a6833440b8e238947e47bea7f11071dc2b2"]) + assert.True(t, depIDs["flake-utils:b1d9ab70662946ef0850d488da1c9019f3a9752a"]) + assert.True(t, depIDs["systems:da67096a3b9bf56a91d16901293e51ba5b49a27e"]) + + // Validate requestedBy: systems should be requested by flake-utils + for _, dep := range module.Dependencies { + if dep.Id == "systems:da67096a3b9bf56a91d16901293e51ba5b49a27e" { + require.NotEmpty(t, dep.RequestedBy) + foundFlakeUtils := false + for _, chain := range dep.RequestedBy { + for _, parent := range chain { + if parent == "flake-utils:b1d9ab70662946ef0850d488da1c9019f3a9752a" { + foundFlakeUtils = true + } + } + } + assert.True(t, foundFlakeUtils, "systems should be requested by flake-utils") + } + } + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) +} diff --git a/testdata/nix/nixproject/flake.lock b/testdata/nix/nixproject/flake.lock new file mode 100644 index 000000000..26df016bb --- /dev/null +++ b/testdata/nix/nixproject/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1710272261, + "narHash": "sha256-g+z7DFEIGGxPcQ4kDsSlFNzXJVhqPiGMrx0cPYrGnNA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0ad13a6833440b8e238947e47bea7f11071dc2b2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/testdata/nix/nixproject/flake.nix b/testdata/nix/nixproject/flake.nix new file mode 100644 index 000000000..7054471ab --- /dev/null +++ b/testdata/nix/nixproject/flake.nix @@ -0,0 +1,21 @@ +{ + description = "Test flake project for JFrog CLI Nix integration tests"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + packages.default = pkgs.hello; + devShells.default = pkgs.mkShell { + buildInputs = [ pkgs.hello ]; + }; + } + ); +} diff --git a/testdata/nix_local_repository_config.json b/testdata/nix_local_repository_config.json new file mode 100644 index 000000000..a2a656230 --- /dev/null +++ b/testdata/nix_local_repository_config.json @@ -0,0 +1,5 @@ +{ + "key": "${NIX_LOCAL_REPO}", + "rclass": "local", + "packageType": "nix" +} diff --git a/testdata/nix_remote_repository_config.json b/testdata/nix_remote_repository_config.json new file mode 100644 index 000000000..03648b1b4 --- /dev/null +++ b/testdata/nix_remote_repository_config.json @@ -0,0 +1,6 @@ +{ + "key": "${NIX_REMOTE_REPO}", + "rclass": "remote", + "packageType": "nix", + "url": "https://cache.nixos.org" +} diff --git a/testdata/nix_virtual_repository_config.json b/testdata/nix_virtual_repository_config.json new file mode 100644 index 000000000..e530dbc9f --- /dev/null +++ b/testdata/nix_virtual_repository_config.json @@ -0,0 +1,8 @@ +{ + "key": "${NIX_VIRTUAL_REPO}", + "rclass": "virtual", + "packageType": "nix", + "repositories": [ + "${NIX_REMOTE_REPO}" + ] +} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 60e5c9a8c..031abedd1 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -88,6 +88,7 @@ const ( RubyConfig = "ruby-config" ConanConfig = "conan-config" Conan = "conan" + Nix = "nix" Ping = "ping" RtCurl = "rt-curl" TemplateConsumer = "template-consumer" @@ -2147,6 +2148,9 @@ var commandFlags = map[string][]string{ Conan: { BuildName, BuildNumber, module, Project, }, + Nix: { + BuildName, BuildNumber, module, Project, serverId, + }, Stats: { XrFormat, accessToken, serverId, }, diff --git a/utils/tests/consts.go b/utils/tests/consts.go index d68e9b2f5..5737fc7be 100644 --- a/utils/tests/consts.go +++ b/utils/tests/consts.go @@ -105,6 +105,9 @@ const ( PypiLocalRepositoryConfig = "pypi_local_repository_config.json" PypiRemoteRepositoryConfig = "pypi_remote_repository_config.json" PypiVirtualRepositoryConfig = "pypi_virtual_repository_config.json" + NixLocalRepositoryConfig = "nix_local_repository_config.json" + NixRemoteRepositoryConfig = "nix_remote_repository_config.json" + NixVirtualRepositoryConfig = "nix_virtual_repository_config.json" PoetryLocalRepositoryConfig = "poetry_local_repository_config.json" PoetryRemoteRepositoryConfig = "poetry_remote_repository_config.json" PoetryVirtualRepositoryConfig = "poetry_virtual_repository_config.json" @@ -210,6 +213,9 @@ var ( PypiVirtualRepo = "cli-pypi-virtual" PipenvRemoteRepo = "cli-pipenv-pypi-remote" PipenvVirtualRepo = "cli-pipenv-pypi-virtual" + NixLocalRepo = "cli-nix-local" + NixRemoteRepo = "cli-nix-remote" + NixVirtualRepo = "cli-nix-virtual" PoetryLocalRepo = "cli-poetry-local" PoetryRemoteRepo = "cli-poetry-remote" PoetryVirtualRepo = "cli-poetry-virtual"