Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 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
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
6 changes: 5 additions & 1 deletion bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ workflows:
- bundle::run_integration_tests: { }
meta:
bitrise.io:
stack: osx-xcode-16.4.x
stack: osx-xcode-edge # TODO: temporary
machine_type_id: g2.mac.large

run_integration_tests_linux:
Expand All @@ -86,6 +86,10 @@ workflows:
- workflow_id: unpin-go-version
- bundle::setup_go_junit_report: { }
- bundle::run_integration_tests: { }
meta:
bitrise.io:
stack: ubuntu-noble-24.04-bitrise-2025-android
machine_type_id: g2.linux.x-large

run_docker_integration_tests_linux:
steps:
Expand Down
90 changes: 90 additions & 0 deletions integrationtests/toolprovider/mise/install_nixpkgs_ruby_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//go:build linux_and_mac
// +build linux_and_mac

package mise

import (
"testing"

"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
tool string
version string
resolutionStrategy provider.ResolutionStrategy
want string
wantErr bool
}{
{
name: "Install specific version",
tool: "ruby",
version: "3.3.9",
resolutionStrategy: provider.ResolutionStrategyStrict,
want: "3.3.9",
},
{
name: "Install fuzzy version and released strategy",
tool: "ruby",
version: "3.1", // EOL version, won't receive new patch versions suddenly
resolutionStrategy: provider.ResolutionStrategyLatestReleased,
want: "3.1.7",
},
{
name: "Install fuzzy version and installed strategy",
tool: "ruby",
version: "3.1", // EOL version, won't receive new patch versions
resolutionStrategy: provider.ResolutionStrategyLatestInstalled,
want: "3.1.7",
},
{
name: "Nonexistent version in nixpkgs index",
tool: "ruby",
version: "0.1.999",
resolutionStrategy: provider.ResolutionStrategyStrict,
wantErr: true,
},
{
name: "Install some other tool with forced nixpkgs backend",
tool: "node",
version: "22.22.1",
resolutionStrategy: provider.ResolutionStrategyStrict,
wantErr: true,
},
}

t.Setenv("BITRISE_TOOLSETUP_FAST_INSTALL", "true")
t.Setenv("BITRISE_TOOLSETUP_FAST_INSTALL_FORCE", "true")

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) {
request := provider.ToolRequest{
ToolName: "ruby",
UnparsedVersion: tt.version,
ResolutionStrategy: tt.resolutionStrategy,
}
result, installErr := miseProvider.InstallTool(request)

if tt.wantErr {
require.Error(t, installErr)
return
}
require.NoError(t, installErr)
require.Equal(t, provider.ToolID("ruby"), result.ToolName)
require.Equal(t, tt.want, result.ConcreteVersion)
require.False(t, result.IsAlreadyInstalled)
})
}
}
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
36 changes: 27 additions & 9 deletions toolprovider/mise/execenv/execenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,33 @@ const (

// ExecEnv contains everything needed to run mise commands in a specific environment
// that is installed and pre-configured.
type ExecEnv struct {
type ExecEnv interface {
// InstallDir is the directory where mise is installed. This is not necessarily the same as the data directory.
InstallDir string
InstallDir() string
RunMise(args ...string) (string, error)
RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error)
RunMisePlugin(args ...string) (string, error)
}

type MiseExecEnv struct {
installDir string

// Additional env vars that configure mise and are required for its operation.
ExtraEnvs map[string]string
extraEnvs map[string]string
}

func (e *ExecEnv) RunMise(args ...string) (string, error) {
// extraEnvs: additional env vars that configure mise and are required for its operation.
func NewMiseExecEnv(installDir string, extraEnvs map[string]string) MiseExecEnv {
return MiseExecEnv{
installDir: installDir,
extraEnvs: extraEnvs,
}
}

func (e MiseExecEnv) RunMise(args ...string) (string, error) {
return e.RunMiseWithTimeout(0, args...)
}

func (e *ExecEnv) RunMisePlugin(args ...string) (string, error) {
func (e MiseExecEnv) RunMisePlugin(args ...string) (string, error) {
cmdWithArgs := append([]string{"plugin"}, args...)

// Use timeout for all plugin operations as they involve unknown code execution
Expand All @@ -37,7 +51,7 @@ func (e *ExecEnv) RunMisePlugin(args ...string) (string, error) {

// RunMiseWithTimeout runs mise commands that involve untrusted operations (plugin execution, remote network calls)
// with a timeout to prevent hanging
func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
func (e MiseExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
var ctx context.Context
if timeout == 0 {
ctx = context.Background()
Expand All @@ -47,10 +61,10 @@ func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (str
defer cancel()
}

executable := filepath.Join(e.InstallDir, "bin", "mise")
executable := filepath.Join(e.installDir, "bin", "mise")
cmd := exec.CommandContext(ctx, executable, args...)
cmd.Env = os.Environ()
for k, v := range e.ExtraEnvs {
for k, v := range e.extraEnvs {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
output, err := cmd.CombinedOutput()
Expand All @@ -63,3 +77,7 @@ func (e *ExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (str

return string(output), nil
}

func (e MiseExecEnv) InstallDir() string {
return e.installDir
}
59 changes: 59 additions & 0 deletions toolprovider/mise/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package mise

import (
"fmt"
"strings"
"time"
)

type fakeExecEnv struct {
// responses maps command strings to their outputs
responses map[string]string
// errors maps command strings to errors
errors map[string]error
}

func newFakeExecEnv() *fakeExecEnv {
return &fakeExecEnv{
responses: make(map[string]string),
errors: make(map[string]error),
}
}

func (m *fakeExecEnv) setResponse(cmdKey string, output string) {
m.responses[cmdKey] = output
}

func (m *fakeExecEnv) setError(cmdKey string, err error) {
m.errors[cmdKey] = err
}

func (m *fakeExecEnv) InstallDir() string {
return "/fake/mise/install/dir"
}

func (m *fakeExecEnv) RunMise(args ...string) (string, error) {
return m.runCommand(args...)
}

func (m *fakeExecEnv) RunMisePlugin(args ...string) (string, error) {
return m.runCommand(append([]string{"plugin"}, args...)...)
}

func (m *fakeExecEnv) RunMiseWithTimeout(timeout time.Duration, args ...string) (string, error) {
return m.runCommand(args...)
}

func (m *fakeExecEnv) runCommand(args ...string) (string, error) {
cmdKey := strings.Join(args, " ")

if err, ok := m.errors[cmdKey]; ok {
return "", err
}

if output, ok := m.responses[cmdKey]; ok {
return output, nil
}

return "", fmt.Errorf("no mock response configured for command: %s", cmdKey)
}
48 changes: 48 additions & 0 deletions toolprovider/mise/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,59 @@ package mise
import (
"errors"
"fmt"
"os"

"github.com/bitrise-io/bitrise/v2/log"
"github.com/bitrise-io/bitrise/v2/toolprovider/mise/execenv"
"github.com/bitrise-io/bitrise/v2/toolprovider/mise/nixpkgs"
"github.com/bitrise-io/bitrise/v2/toolprovider/provider"
)

func installRequest(toolRequest provider.ToolRequest, useNix bool) provider.ToolRequest {
if useNix {
return provider.ToolRequest{
// Use Mise's backend plugin convention of pluginID:toolID
ToolName: provider.ToolID(fmt.Sprintf("%s:%s", nixpkgs.PluginName, toolRequest.ToolName)),
UnparsedVersion: toolRequest.UnparsedVersion,
ResolutionStrategy: toolRequest.ResolutionStrategy,
PluginURL: nil, // Not relevant when using nixpkgs backend plugin
}
} else {
return toolRequest
}
}

func canBeInstalledWithNix(tool provider.ToolRequest, execEnv execenv.ExecEnv) bool {
// Force switch for integration testing. No fallback to regular install when this is active. This makes failures explicit.
forceNix := os.Getenv("BITRISE_TOOLSETUP_FAST_INSTALL_FORCE") == "true"
if forceNix {
return true
}

if !nixpkgs.ShouldUseBackend(tool) {
return false
}

_, err := execEnv.RunMisePlugin("install", nixpkgs.PluginName, nixpkgs.PluginGitURL)
if err != nil {
log.Warnf("Error while installing nixpkgs plugin (%s). Falling back to core plugin installation.", nixpkgs.PluginGitURL, err)
return false
}

nameWithBackend := provider.ToolID(fmt.Sprintf("nixpkgs:%s", tool.ToolName))
available, err := versionExists(execEnv, nameWithBackend, tool.UnparsedVersion)
if err != nil {
log.Warnf("Error while checking nixpkgs index for %s@%s: %v. Falling back to core plugin installation.", tool.ToolName, tool.UnparsedVersion, err)
return false
}
if !available {
log.Warnf("%s@%s not found in nixpkgs index, doing a source build. This may take some time...", tool.ToolName, tool.UnparsedVersion)
return false
}

return true
}

func (m *MiseToolProvider) installToolVersion(tool provider.ToolRequest) error {
versionString, err := miseVersionString(tool, m.resolveToLatestInstalled)
if err != 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
Loading
Loading