Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
081f066
[BE-1876] Integrate nixpkgs plugin
marcell-vida Nov 11, 2025
eb05dd2
Check for version in index, remove plugin version pinning, fixes
marcell-vida Nov 11, 2025
1dfba3d
*
marcell-vida Nov 12, 2025
3738f29
Fix test version
marcell-vida Nov 12, 2025
ebe94d2
Rename tool
marcell-vida Nov 12, 2025
fce56eb
Remove index checking, use listing
marcell-vida Nov 12, 2025
d4c3efd
*
marcell-vida Nov 12, 2025
c177efe
Timeout on test
marcell-vida Nov 12, 2025
9aaccad
Simplify things
ofalvai Nov 13, 2025
95548fe
Get rid of MiseExecutor, make ExecEnv mockable instead
ofalvai Nov 14, 2025
adc95ec
Change plugin ID to final value
ofalvai Nov 14, 2025
24fd4b5
Unit tests
ofalvai Nov 14, 2025
aca4ce3
Add force flag for integration testing
ofalvai Nov 14, 2025
a2d8363
Integration tests
ofalvai Nov 14, 2025
972e50b
Add check before executing nix commands
ofalvai Nov 17, 2025
59c8133
Make feature flag value true instead of 1
ofalvai Nov 17, 2025
7e4bff6
AI reviewer fixes
marcell-vida Nov 18, 2025
945d3a3
Merge branch 'master' into BE-1876-integrate-nixpkgs-plugin
marcell-vida Nov 18, 2025
7d750f5
chore: trigger CI checks
marcell-vida Nov 18, 2025
d6e6b20
Raise timeout, install backend even with force install
marcell-vida Nov 18, 2025
67f994c
Lint
marcell-vida Nov 19, 2025
6f8f212
Fix tests, update plugin
marcell-vida Nov 19, 2025
488b138
Decrease validate timeout
marcell-vida Nov 19, 2025
45d52d4
Fix tests
marcell-vida Nov 19, 2025
dec4eb0
*
marcell-vida Nov 20, 2025
28bb814
Tests working on stable stacks too, consistent commenting
marcell-vida Nov 21, 2025
68e3e49
Remove nix availability check
marcell-vida Nov 21, 2025
e9574f3
Fake nix checking in tests instead of env var override
ofalvai Nov 24, 2025
ba0feab
Merge branch 'master' into BE-1876-integrate-nixpkgs-plugin
marcell-vida Nov 24, 2025
3138f4d
Fixes
marcell-vida Nov 25, 2025
77690fa
Comments
ofalvai Nov 25, 2025
de3808a
*
marcell-vida Nov 25, 2025
2c1987b
Merge branch 'master' into BE-1876-integrate-nixpkgs-plugin
ofalvai Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions integrationtests/toolprovider/mise/install_nixpkgs_ruby_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build linux_and_mac
// +build linux_and_mac

package mise

import (
"context"
"testing"
"time"

"github.com/bitrise-io/bitrise/v2/toolprovider/mise"
"github.com/bitrise-io/bitrise/v2/toolprovider/provider"
"github.com/stretchr/testify/require"
)

func TestMiseInstallNixpkgsRuby(t *testing.T) {
tests := []struct {
name string
requestedVersion string
resolutionStrategy provider.ResolutionStrategy
expectedVersion string
}{
{"Install specific version", "3.3.9", provider.ResolutionStrategyStrict, "3.3.9"},
}

for _, tt := range tests {
miseInstallDir := t.TempDir()
miseDataDir := t.TempDir()
miseProvider, err := mise.NewToolProvider(miseInstallDir, miseDataDir)
require.NoError(t, err)

err = miseProvider.Bootstrap()
require.NoError(t, err)

t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

done := make(chan bool)
var result provider.ToolInstallResult
var installErr error

go func() {
request := provider.ToolRequest{
ToolName: "ruby",
UnparsedVersion: tt.requestedVersion,
ResolutionStrategy: tt.resolutionStrategy,
}
result, installErr = miseProvider.InstallTool(request)
done <- true
}()

select {
case <-done:
require.NoError(t, installErr)
require.Equal(t, provider.ToolID("ruby"), result.ToolName)
require.Equal(t, tt.expectedVersion, result.ConcreteVersion)
require.False(t, result.IsAlreadyInstalled)
case <-ctx.Done():
t.Fatal("Test exceeded 1 minute timeout, installation was too slow for nixpkgs ruby")
}
})
}
}
2 changes: 1 addition & 1 deletion toolprovider/asdf/install_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (a *AsdfToolProvider) InstallPlugin(tool provider.ToolRequest) error {
log.Warnf("Failed to check if plugin is already installed: %v", err)
}
if installed {
log.Debugf("Tool plugin %s is already installed, skipping installation.", tool.ToolName)
log.Debugf("[TOOLPROVIDER] Tool plugin %s is already installed, skipping installation.", tool.ToolName)
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions toolprovider/mise/install_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (m *MiseToolProvider) InstallPlugin(tool provider.ToolRequest) error {
}
if plugin == nil {
// No plugin installation needed (either core tool or registry tool).
log.Debugf("No plugin installation needed for tool %s", tool.ToolName)
log.Debugf("[TOOLPROVIDER] No plugin installation needed for tool %s", tool.ToolName)
return nil
}

Expand All @@ -53,7 +53,7 @@ func (m *MiseToolProvider) InstallPlugin(tool provider.ToolRequest) error {
log.Warnf("Failed to check if plugin is already installed: %v", err)
}
if installed {
log.Debugf("Tool plugin %s is already installed, skipping installation.", tool.ToolName)
log.Debugf("[TOOLPROVIDER] Tool plugin %s is already installed, skipping installation.", tool.ToolName)
return nil
}

Expand Down
10 changes: 10 additions & 0 deletions toolprovider/mise/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ func TestMiseVersionString(t *testing.T) {
want: "",
wantErr: true,
},
{
name: "strict resolution with nixpkgs backend",
tool: provider.ToolRequest{
ToolName: "nixpkgs:ruby",
UnparsedVersion: "3.3.0",
ResolutionStrategy: provider.ResolutionStrategyStrict,
},
want: "nixpkgs:[email protected]",
wantErr: false,
},
}

for _, tt := range tests {
Expand Down
150 changes: 137 additions & 13 deletions toolprovider/mise/mise.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

Expand Down Expand Up @@ -40,6 +41,11 @@ var miseStableChecksums = map[string]string{
"macos-arm64": "0b5893de7c8c274736867b7c4c7ed565b4429f4d6272521ace802f8a21422319",
}

const (
nixpkgsPluginGitURL = "https://github.com/bitrise-io/mise-nixpkgs-plugin.git"
nixpkgsPluginName = "mise-nixpkgs-plugin"
)

type MiseToolProvider struct {
ExecEnv execenv.ExecEnv
}
Expand Down Expand Up @@ -102,25 +108,34 @@ func (m *MiseToolProvider) Bootstrap() error {
}

func (m *MiseToolProvider) InstallTool(tool provider.ToolRequest) (provider.ToolInstallResult, error) {
err := m.InstallPlugin(tool)
if err != nil {
return provider.ToolInstallResult{}, fmt.Errorf("install tool plugin %s: %w", tool.ToolName, err)
useNix := m.shouldUseNixPkgs(tool)

if useNix {
tool.ToolName = provider.ToolID(fmt.Sprintf("nixpkgs:%s", tool.ToolName))
} else {
err := m.InstallPlugin(tool)
if err != nil {
return provider.ToolInstallResult{}, fmt.Errorf("install tool plugin %s: %w", tool.ToolName, err)
}
}

isAlreadyInstalled, err := isAlreadyInstalled(tool, m.resolveToLatestInstalled)
if err != nil {
return provider.ToolInstallResult{}, err
}

versionExists, err := m.versionExists(tool.ToolName, tool.UnparsedVersion)
if err != nil {
return provider.ToolInstallResult{}, fmt.Errorf("check if version exists: %w", err)
}
if !versionExists {
return provider.ToolInstallResult{}, provider.ToolInstallError{
ToolName: tool.ToolName,
RequestedVersion: tool.UnparsedVersion,
Cause: fmt.Sprintf("no match for requested version %s", tool.UnparsedVersion),
if !useNix {
// Nix already checks version existence previously
versionExists, err := m.versionExists(tool.ToolName, tool.UnparsedVersion)
if err != nil {
return provider.ToolInstallResult{}, fmt.Errorf("check if version exists: %w", err)
}
if !versionExists {
return provider.ToolInstallResult{}, provider.ToolInstallError{
ToolName: tool.ToolName,
RequestedVersion: tool.UnparsedVersion,
Cause: fmt.Sprintf("no match for requested version %s", tool.UnparsedVersion),
}
}
}

Expand Down Expand Up @@ -160,7 +175,7 @@ func isEdgeStack() (isEdge bool) {
} else {
isEdge = false
}
log.Debugf("Mise: Stack is edge: %s", isEdge)
log.Debugf("[TOOLPROVIDER] Stack is edge: %s", isEdge)
return
}

Expand All @@ -179,3 +194,112 @@ func GetMiseChecksums() map[string]string {
// Fallback to stable version for non-edge stacks
return miseStableChecksums
}

func useNixPkgs(tool provider.ToolRequest) bool {
// Note: Add other tools here if needed
if tool.ToolName != "ruby" {
log.Debugf("[TOOLPROVIDER] Nix packages are only supported for ruby tool, current tool: %s", tool.ToolName)
return false
}

if value, variablePresent := os.LookupEnv("BITRISEIO_MISE_LEGACY_INSTALL"); variablePresent && strings.Contains(value, "1") {
log.Debugf("[TOOLPROVIDER] Using legacy install (non-nix) for tool: %s", tool.ToolName)
return false
}

return true
}

// shouldUseNixPkgs checks if Nix packages should be used for the tool installation.
// It validates that the tool is eligible for Nix, the plugin is available, and the version exists in the index.
// Returns false and logs a warning if any validation fails, falling back to legacy installation.
func (m *MiseToolProvider) shouldUseNixPkgs(tool provider.ToolRequest) bool {
if !useNixPkgs(tool) {
return false
}

if err := m.getNixpkgsPlugin(); err != nil {
log.Warnf("Failed to link nixpkgs plugin: %v. Falling back to legacy installation.", err)
return false
}

nameWithBackend := provider.ToolID(fmt.Sprintf("nixpkgs:%s", tool.ToolName))

available, err := m.versionExists(nameWithBackend, tool.UnparsedVersion)
if err != nil || !available {
log.Warnf("Failed to check nixpkgs index for %s@%s: %v. Falling back to legacy installation.", tool.ToolName, tool.UnparsedVersion, err)
return false
}

return true
}

// findPluginPath finds the nixpkgs plugin directory relative to the bitrise executable.
// It first checks next to the binary, then tries a dev location one directory up.
// Returns the path if found, otherwise returns an error.
func findPluginPath() (string, error) {
execPath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("get executable path: %w", err)
}

execDir := filepath.Dir(execPath)
pluginPath := filepath.Join(execDir, nixpkgsPluginName)

// Check if the plugin exists besides the binary
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
// Try dev location
pluginPath = filepath.Join(execDir, "..", nixpkgsPluginName)
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
return "", fmt.Errorf("%s not found", nixpkgsPluginName)
}
}

return pluginPath, nil
}

// getNixpkgsPlugin clones or updates the nixpkgs backend plugin and links it to mise.
// If the plugin directory doesn't exist, it clones from the git URL.
// If it exists, it checks out the specified commit/branch.
func (m *MiseToolProvider) getNixpkgsPlugin() error {
pluginPath, err := findPluginPath()
needsClone := false
if err != nil {
// Plugin doesn't exist, we need to clone it
needsClone = true
execPath, execErr := os.Executable()
if execErr != nil {
return fmt.Errorf("get executable path: %w", execErr)
}
pluginPath = filepath.Join(filepath.Dir(execPath), nixpkgsPluginName)
}

if needsClone {
if err := cloneGitRepo(nixpkgsPluginGitURL, pluginPath); err != nil {
return fmt.Errorf("clone nixpkgs plugin: %w", err)
}
}

// Link the plugin using mise plugin link
_, err = m.ExecEnv.RunMisePlugin("link", "--force", "nixpkgs", pluginPath)
if err != nil {
return fmt.Errorf("link nixpkgs plugin: %w", err)
}

// Enable experimental settings for custom backend
if _, err := m.ExecEnv.RunMise("settings", "experimental=true"); err != nil {
return fmt.Errorf("enable experimental settings: %w", err)
}

return nil
}

// cloneGitRepo clones a git repository to the specified path with minimal history.
func cloneGitRepo(repoURL, destPath string) error {
cmd := exec.Command("git", "clone", "--depth", "1", "--no-tags", repoURL, destPath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git clone failed: %w: %s", err, string(output))
}
return nil
}
Loading