From 27170d06b2b3882783b0913c254ae1860313e54d Mon Sep 17 00:00:00 2001 From: Reshmi Date: Mon, 4 May 2026 10:28:52 +0530 Subject: [PATCH] added the support for nix package manager --- buildtools/cli.go | 39 ++ go.mod | 6 +- go.sum | 4 +- main_test.go | 4 +- nix_test.go | 515 ++++++++++++++++++++ testdata/nix/nixproject/flake.lock | 61 +++ testdata/nix/nixproject/flake.nix | 21 + testdata/nix_local_repository_config.json | 5 + testdata/nix_remote_repository_config.json | 6 + testdata/nix_virtual_repository_config.json | 8 + utils/cliutils/commandsflags.go | 4 + utils/tests/consts.go | 7 + utils/tests/utils.go | 11 + 13 files changed, 683 insertions(+), 8 deletions(-) create mode 100644 nix_test.go create mode 100644 testdata/nix/nixproject/flake.lock create mode 100644 testdata/nix/nixproject/flake.nix create mode 100644 testdata/nix_local_repository_config.json create mode 100644 testdata/nix_remote_repository_config.json create mode 100644 testdata/nix_virtual_repository_config.json diff --git a/buildtools/cli.go b/buildtools/cli.go index 8fa7284eb..4735871a9 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" @@ -53,6 +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/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" @@ -382,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), @@ -1878,6 +1893,30 @@ 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 + } + + cmdName, nixArgs := getCommandName(filteredArgs) + + // Use jfrog-cli-artifactory Nix command with build info support + cmd := nixcommand.NewNixCommand().SetCommandName(cmdName).SetArgs(nixArgs).SetBuildConfiguration(buildConfiguration) + + 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 5a87ff122..9b07edb6f 100644 --- a/go.mod +++ b/go.mod @@ -248,11 +248,9 @@ 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.20260331093138-48a54e89a292 +replace github.com/jfrog/jfrog-cli-artifactory => github.com/reshmifrog/jfrog-cli-artifactory v0.8.1-0.20260504045304-0599c3095faf -//replace github.com/jfrog/build-info-go => github.com/fluxxBot/build-info-go v1.10.10-0.20260105070825-d3f36f619ba5 - -// replace github.com/jfrog/build-info-go => github.com/reshmifrog/build-info-go v1.10.11-0.20260303032831-71878c7210bf +replace github.com/jfrog/build-info-go => github.com/reshmifrog/build-info-go v1.10.11-0.20260504044902-efb70095f1e1 //replace github.com/jfrog/jfrog-cli-core/v2 => github.com/fluxxBot/jfrog-cli-core/v2 v2.58.1-0.20260105065921-c6488910f44c diff --git a/go.sum b/go.sum index 2566c5330..5457c6153 100644 --- a/go.sum +++ b/go.sum @@ -406,7 +406,6 @@ 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.20260331040230-c3b53d1a24ac h1:VKZar+MKKcCoEnT3f1Nq0DkHV07PuI18NEPjlnJCh7M= github.com/jfrog/build-info-go v1.13.1-0.20260331040230-c3b53d1a24ac/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= @@ -418,7 +417,6 @@ 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.20260406055206-755b2b3eb84d h1:yIVNT/vDk4WZVaBOdpOZ8ex8YhywDuH6253edchKHzM= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260406055206-755b2b3eb84d/go.mod h1:KSJZO+tguFpGG4TE2Ut2rmOk1j03RrqHQ7E33FrsEt4= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260402104745-7a0bc2c11d63 h1:rvEiuETYgy7VbQFmf1QeYTcG0Sp4Lr+1QgrVQzLV58Q= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260402104745-7a0bc2c11d63/go.mod h1:RLLUO+oGDq88e5DPtP/KK2sVgMF32OuoRdVMxSFfb30= @@ -573,6 +571,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/reshmifrog/build-info-go v1.10.11-0.20260504044902-efb70095f1e1/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/reshmifrog/jfrog-cli-artifactory v0.8.1-0.20260504045304-0599c3095faf/go.mod h1:M2QgQrSya6BSuLO7qPBDKbJqlQpYKPg0+lqFDDYMbgQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= diff --git a/main_test.go b/main_test.go index 31588dd7a..c66c479bf 100644 --- a/main_test.go +++ b/main_test.go @@ -67,7 +67,7 @@ func setupIntegrationTests() { InitArtifactoryTests() } - if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestNix || *tests.TestConan || *tests.TestHelm || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { InitBuildToolsTests() } if *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan { @@ -106,7 +106,7 @@ func tearDownIntegrationTests() { if (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanArtifactoryTests() } - if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestConan || *tests.TestHelm || *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { + if *tests.TestNpm || *tests.TestPnpm || *tests.TestGradle || *tests.TestMaven || *tests.TestGo || *tests.TestNuget || *tests.TestPip || *tests.TestPipenv || *tests.TestPoetry || *tests.TestNix || *tests.TestConan || *tests.TestHelm || *tests.TestDocker || *tests.TestPodman || *tests.TestDockerScan || (*tests.TestArtifactory && !*tests.TestArtifactoryProxy) || *tests.TestArtifactoryProject { CleanBuildToolsTests() } if *tests.TestDistribution { 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 bd0e845fa..ddf0039fa 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -84,6 +84,7 @@ const ( HuggingFaceDownload = "hugging-face-download" RubyConfig = "ruby-config" Conan = "conan" + Nix = "nix" Ping = "ping" RtCurl = "rt-curl" TemplateConsumer = "template-consumer" @@ -2030,6 +2031,9 @@ var commandFlags = map[string][]string{ Conan: { BuildName, BuildNumber, module, Project, }, + Nix: { + BuildName, BuildNumber, module, Project, + }, Stats: { xrOutput, accessToken, serverId, }, diff --git a/utils/tests/consts.go b/utils/tests/consts.go index fd48adea8..296928aaf 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" @@ -207,6 +210,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" @@ -248,6 +254,7 @@ var ( NuGetBuildName = "cli-nuget-build" PipBuildName = "cli-pip-build" PipenvBuildName = "cli-pipenv-build" + NixBuildName = "cli-nix-build" PoetryBuildName = "cli-poetry-build" ConanBuildName = "cli-conan-build" HelmBuildName = "cli-helm-build" diff --git a/utils/tests/utils.go b/utils/tests/utils.go index f277cb453..1b14d1d22 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -69,6 +69,7 @@ var ( TestPip *bool TestPipenv *bool TestPoetry *bool + TestNix *bool TestConan *bool TestHelm *bool TestHuggingFace *bool @@ -111,6 +112,7 @@ func init() { TestPip = flag.Bool("test.pip", false, "Test Pip") TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") + TestNix = flag.Bool("test.nix", false, "Test Nix") TestConan = flag.Bool("test.conan", false, "Test Conan") TestHelm = flag.Bool("test.helm", false, "Test Helm") TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") @@ -286,6 +288,9 @@ var reposConfigMap = map[*string]string{ &PypiVirtualRepo: PypiVirtualRepositoryConfig, &PipenvRemoteRepo: PipenvRemoteRepositoryConfig, &PipenvVirtualRepo: PipenvVirtualRepositoryConfig, + &NixLocalRepo: NixLocalRepositoryConfig, + &NixRemoteRepo: NixRemoteRepositoryConfig, + &NixVirtualRepo: NixVirtualRepositoryConfig, &PoetryLocalRepo: PoetryLocalRepositoryConfig, &PoetryRemoteRepo: PoetryRemoteRepositoryConfig, &PoetryVirtualRepo: PoetryVirtualRepositoryConfig, @@ -358,6 +363,7 @@ func GetNonVirtualRepositories() map[*string]string { TestNuget: {&NugetRemoteRepo}, TestPip: {&PypiLocalRepo, &PypiRemoteRepo}, TestPipenv: {&PipenvRemoteRepo}, + TestNix: {&NixLocalRepo, &NixRemoteRepo}, TestPoetry: {&PoetryLocalRepo, &PoetryRemoteRepo}, TestConan: {&ConanLocalRepo, &ConanRemoteRepo}, TestHelm: {&HelmLocalRepo}, @@ -388,6 +394,7 @@ func GetVirtualRepositories() map[*string]string { TestNuget: {}, TestPip: {&PypiVirtualRepo}, TestPipenv: {&PipenvVirtualRepo}, + TestNix: {&NixVirtualRepo}, TestPoetry: {&PoetryVirtualRepo}, TestConan: {&ConanVirtualRepo}, TestHelm: {}, @@ -429,6 +436,7 @@ func GetBuildNames() []string { TestNuget: {&NuGetBuildName}, TestPip: {&PipBuildName}, TestPipenv: {&PipenvBuildName}, + TestNix: {&NixBuildName}, TestPoetry: {&PoetryBuildName}, TestConan: {&ConanBuildName}, TestHelm: {&HelmBuildName}, @@ -487,6 +495,9 @@ func getSubstitutionMap() map[string]string { "${PYPI_VIRTUAL_REPO}": PypiVirtualRepo, "${PIPENV_REMOTE_REPO}": PipenvRemoteRepo, "${PIPENV_VIRTUAL_REPO}": PipenvVirtualRepo, + "${NIX_LOCAL_REPO}": NixLocalRepo, + "${NIX_REMOTE_REPO}": NixRemoteRepo, + "${NIX_VIRTUAL_REPO}": NixVirtualRepo, "${POETRY_LOCAL_REPO}": PoetryLocalRepo, "${POETRY_REMOTE_REPO}": PoetryRemoteRepo, "${POETRY_VIRTUAL_REPO}": PoetryVirtualRepo,