Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
15 changes: 13 additions & 2 deletions toolprovider/mise/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import (
)

func (m *MiseToolProvider) installToolVersion(tool provider.ToolRequest) error {
versionString, err := miseVersionString(tool, m.resolveToLatestInstalled)
backend := ""
if useNixPkgs(tool) {
backend = "nixpkgs"
}

versionString, err := miseVersionString(tool, m.resolveToLatestInstalled, backend)
if err != nil {
return err
}
Expand Down Expand Up @@ -46,7 +51,7 @@ func isAlreadyInstalled(tool provider.ToolRequest, latestInstalledResolver lates
return isAlreadyInstalled, nil
}

func miseVersionString(tool provider.ToolRequest, latestInstalledResolver latestInstalledResolver) (string, error) {
func miseVersionString(tool provider.ToolRequest, latestInstalledResolver latestInstalledResolver, backend string) (string, error) {
var miseVersionString string
resolutionStrategy := tool.ResolutionStrategy
if tool.UnparsedVersion == "installed" {
Expand Down Expand Up @@ -74,6 +79,12 @@ func miseVersionString(tool provider.ToolRequest, latestInstalledResolver latest
default:
return "", fmt.Errorf("unknown resolution strategy: %v", tool.ResolutionStrategy)
}

if backend != "" {
// https://mise.jdx.dev/configuration.html#backends
miseVersionString = fmt.Sprintf("%s:%s", backend, miseVersionString)
}

return miseVersionString, nil

}
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
31 changes: 30 additions & 1 deletion toolprovider/mise/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func TestMiseVersionString(t *testing.T) {
tests := []struct {
name string
tool provider.ToolRequest
backend string
want string
wantErr bool
}{
Expand All @@ -23,6 +24,7 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "18.20.0",
ResolutionStrategy: provider.ResolutionStrategyStrict,
},
backend: "",
want: "[email protected]",
wantErr: false,
},
Expand All @@ -33,6 +35,7 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "3.11",
ResolutionStrategy: provider.ResolutionStrategyLatestReleased,
},
backend: "",
want: "python@prefix:3.11",
wantErr: false,
},
Expand All @@ -43,6 +46,7 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "1.21",
ResolutionStrategy: provider.ResolutionStrategyLatestInstalled,
},
backend: "",
want: "[email protected]",
wantErr: false,
},
Expand All @@ -53,6 +57,7 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "17",
ResolutionStrategy: provider.ResolutionStrategyLatestInstalled,
},
backend: "",
want: "java@prefix:17",
wantErr: false,
},
Expand All @@ -63,6 +68,7 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "3.0",
ResolutionStrategy: provider.ResolutionStrategyLatestInstalled,
},
backend: "",
want: "",
wantErr: true,
},
Expand All @@ -73,9 +79,32 @@ func TestMiseVersionString(t *testing.T) {
UnparsedVersion: "18.0.0",
ResolutionStrategy: provider.ResolutionStrategy(999),
},
backend: "",
want: "",
wantErr: true,
},
{
name: "strict resolution with nixpkgs backend",
tool: provider.ToolRequest{
ToolName: "ruby",
UnparsedVersion: "3.3.0",
ResolutionStrategy: provider.ResolutionStrategyStrict,
},
backend: "nixpkgs",
want: "nixpkgs:[email protected]",
wantErr: false,
},
{
name: "latest released with nixpkgs backend",
tool: provider.ToolRequest{
ToolName: "ruby",
UnparsedVersion: "3.3",
ResolutionStrategy: provider.ResolutionStrategyLatestReleased,
},
backend: "nixpkgs",
want: "nixpkgs:ruby@prefix:3.3",
wantErr: false,
},
}

for _, tt := range tests {
Expand All @@ -96,7 +125,7 @@ func TestMiseVersionString(t *testing.T) {
return "", fmt.Errorf("no fake behavior defined for tool %s", toolName)
}

got, err := miseVersionString(tt.tool, latestInstalledResolver)
got, err := miseVersionString(tt.tool, latestInstalledResolver, tt.backend)

if tt.wantErr {
require.Error(t, err)
Expand Down
114 changes: 110 additions & 4 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,9 @@ var miseStableChecksums = map[string]string{
"macos-arm64": "0b5893de7c8c274736867b7c4c7ed565b4429f4d6272521ace802f8a21422319",
}

const nixpkgsPluginGitURL = "https://github.com/bitrise-io/mise-nixpkgs-plugin.git"
const nixpkgsPluginCommit = "7e688b5a79e8aa4b083ad58636f0912440f6d170" // Can be a branch, tag, or commit hash

type MiseToolProvider struct {
ExecEnv execenv.ExecEnv
}
Expand Down Expand Up @@ -102,9 +106,22 @@ 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 := useNixPkgs(tool)

if useNix {
if err := m.getNixpkgsPlugin(); err != nil {
log.Warnf("Failed to link nixpkgs plugin: %v. Falling back to legacy installation.", err)
// Disable Nix package usage
useNix = false
}
// TODO: custom check for version in index
}

if !useNix {
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)
Expand Down Expand Up @@ -160,7 +177,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 +196,92 @@ 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
}

// 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 {
// Find bitrise binary path
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("get executable path: %w", err)
}

execDir := filepath.Dir(execPath)
pluginPath := filepath.Join(execDir, "mise-nixpkgs-plugin")

// Check if the plugin directory exists besides the binary
needsClone := false
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
// Try dev location
pluginPath = filepath.Join(execDir, "..", "mise-nixpkgs-plugin")
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
needsClone = true
}
}

if needsClone {
if err := cloneGitRepo(nixpkgsPluginGitURL, pluginPath, nixpkgsPluginCommit); err != nil {
return fmt.Errorf("clone nixpkgs plugin: %w", err)
}
} else {
if err := checkoutGitCommit(pluginPath, nixpkgsPluginCommit); err != nil {
log.Warnf("Failed to checkout nixpkgs plugin commit: %v. Using current version.", 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)
}

return nil
}

// TODO
// func versionIsAvailableInIndex(toolName provider.ToolID, version string) (bool, error) {
// }

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

// checkoutGitCommit checks out a specific commit, branch, or tag in a git repository.
func checkoutGitCommit(repoPath, commitRef string) error {
fetchCmd := exec.Command("git", "fetch", "--depth", "1", "origin", commitRef)
fetchCmd.Dir = repoPath
if output, err := fetchCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git fetch failed: %w: %s", err, string(output))
}

// Reset to the fetched ref
resetCmd := exec.Command("git", "reset", "--hard", "FETCH_HEAD")
resetCmd.Dir = repoPath
output, err := resetCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git reset failed: %w: %s", err, string(output))
}
return nil
}