feat(capabilities): enhanced tool and platform detection
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
macOS:
- Xcode version and build detection
- iOS/watchOS/tvOS SDK detection
- iOS Simulator detection
- Swift, CocoaPods, Carthage, fastlane detection
- Code signing tools (codesign, notarytool)
- Package builders (pkgbuild, create-dmg)
Windows:
- Visual Studio detection via vswhere
- MSBuild detection
- Inno Setup (ISCC) detection
- NSIS (makensis) detection
- WiX Toolset detection
- Windows SDK signtool detection
- Package managers (Chocolatey, Scoop, winget)
Linux:
- GCC/Clang compiler detection
- Build tools (autoconf, automake, meson)
- Package builders (dpkg-deb, rpmbuild, fpm)
- AppImage tools detection
Cross-platform:
- Ruby, PHP, Swift, Kotlin, Flutter, Dart
- CMake, Make, Ninja, Gradle, Maven
- npm, yarn, pnpm, cargo, pip
- Git version detection
Suggested labels now include:
- xcode, ios, ios-simulator for macOS with Xcode
- inno-setup, nsis, msbuild, vs2022 for Windows
- Tool-based labels (dotnet, java, node)
🤖 Generated with Claude Code
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -31,16 +32,27 @@ type DistroInfo struct {
|
||||
PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS"
|
||||
}
|
||||
|
||||
// XcodeInfo holds Xcode and iOS development information
|
||||
type XcodeInfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
SDKs []string `json:"sdks,omitempty"` // e.g., ["iOS 17.0", "macOS 14.0"]
|
||||
Simulators []string `json:"simulators,omitempty"` // Available iOS simulators
|
||||
}
|
||||
|
||||
// RunnerCapabilities represents the capabilities of a runner for AI consumption
|
||||
type RunnerCapabilities struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Distro *DistroInfo `json:"distro,omitempty"`
|
||||
Xcode *XcodeInfo `json:"xcode,omitempty"`
|
||||
Docker bool `json:"docker"`
|
||||
DockerCompose bool `json:"docker_compose"`
|
||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||
Shell []string `json:"shell,omitempty"`
|
||||
Tools map[string][]string `json:"tools,omitempty"`
|
||||
BuildTools []string `json:"build_tools,omitempty"` // Available build/installer tools
|
||||
PackageManagers []string `json:"package_managers,omitempty"`
|
||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Disk *DiskInfo `json:"disk,omitempty"`
|
||||
@@ -59,10 +71,12 @@ type CapabilityFeatures struct {
|
||||
// DetectCapabilities detects the runner's capabilities
|
||||
func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilities {
|
||||
cap := &RunnerCapabilities{
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Tools: make(map[string][]string),
|
||||
Shell: detectShells(),
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
Tools: make(map[string][]string),
|
||||
BuildTools: []string{},
|
||||
PackageManagers: []string{},
|
||||
Shell: detectShells(),
|
||||
Features: &CapabilityFeatures{
|
||||
ArtifactsV4: false, // Gitea doesn't support v4 artifacts
|
||||
Cache: true,
|
||||
@@ -80,6 +94,11 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
|
||||
cap.Distro = detectLinuxDistro()
|
||||
}
|
||||
|
||||
// Detect macOS Xcode/iOS
|
||||
if runtime.GOOS == "darwin" {
|
||||
cap.Xcode = detectXcode(ctx)
|
||||
}
|
||||
|
||||
// Detect Docker
|
||||
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
|
||||
if cap.Docker {
|
||||
@@ -90,6 +109,12 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
|
||||
// Detect common tools
|
||||
detectTools(ctx, cap)
|
||||
|
||||
// Detect build tools
|
||||
detectBuildTools(ctx, cap)
|
||||
|
||||
// Detect package managers
|
||||
detectPackageManagers(ctx, cap)
|
||||
|
||||
// Detect disk space
|
||||
cap.Disk = detectDiskSpace()
|
||||
|
||||
@@ -99,6 +124,85 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
|
||||
return cap
|
||||
}
|
||||
|
||||
// detectXcode detects Xcode and iOS development capabilities on macOS
|
||||
func detectXcode(ctx context.Context) *XcodeInfo {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Check for xcodebuild
|
||||
cmd := exec.CommandContext(timeoutCtx, "xcodebuild", "-version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
xcode := &XcodeInfo{}
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Xcode ") {
|
||||
xcode.Version = strings.TrimPrefix(line, "Xcode ")
|
||||
} else if strings.HasPrefix(line, "Build version ") {
|
||||
xcode.Build = strings.TrimPrefix(line, "Build version ")
|
||||
}
|
||||
}
|
||||
|
||||
// Get available SDKs
|
||||
cmd = exec.CommandContext(timeoutCtx, "xcodebuild", "-showsdks")
|
||||
output, err = cmd.Output()
|
||||
if err == nil {
|
||||
lines = strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Look for SDK lines like "-sdk iphoneos17.0" or "iOS 17.0"
|
||||
if strings.Contains(line, "SDK") || strings.HasPrefix(line, "-sdk") {
|
||||
continue // Skip header lines
|
||||
}
|
||||
if strings.Contains(line, "iOS") || strings.Contains(line, "macOS") ||
|
||||
strings.Contains(line, "watchOS") || strings.Contains(line, "tvOS") {
|
||||
// Extract SDK name
|
||||
if idx := strings.Index(line, "-sdk"); idx != -1 {
|
||||
sdkPart := strings.TrimSpace(line[:idx])
|
||||
if sdkPart != "" {
|
||||
xcode.SDKs = append(xcode.SDKs, sdkPart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get available simulators
|
||||
cmd = exec.CommandContext(timeoutCtx, "xcrun", "simctl", "list", "devices", "available", "-j")
|
||||
output, err = cmd.Output()
|
||||
if err == nil {
|
||||
var simData struct {
|
||||
Devices map[string][]struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
} `json:"devices"`
|
||||
}
|
||||
if json.Unmarshal(output, &simData) == nil {
|
||||
seen := make(map[string]bool)
|
||||
for runtime, devices := range simData.Devices {
|
||||
if strings.Contains(runtime, "iOS") {
|
||||
for _, dev := range devices {
|
||||
key := dev.Name
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
xcode.Simulators = append(xcode.Simulators, dev.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if xcode.Version == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return xcode
|
||||
}
|
||||
|
||||
// detectLinuxDistro reads /etc/os-release to get distribution info
|
||||
func detectLinuxDistro() *DistroInfo {
|
||||
file, err := os.Open("/etc/os-release")
|
||||
@@ -160,10 +264,50 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
|
||||
addLabel(distro + "-latest")
|
||||
}
|
||||
|
||||
// Xcode/iOS labels (macOS only)
|
||||
if cap.Xcode != nil {
|
||||
addLabel("xcode")
|
||||
// Check for iOS SDK
|
||||
for _, sdk := range cap.Xcode.SDKs {
|
||||
if strings.Contains(strings.ToLower(sdk), "ios") {
|
||||
addLabel("ios")
|
||||
break
|
||||
}
|
||||
}
|
||||
// If simulators available, add simulator label
|
||||
if len(cap.Xcode.Simulators) > 0 {
|
||||
addLabel("ios-simulator")
|
||||
}
|
||||
}
|
||||
|
||||
// Tool-based labels
|
||||
if _, ok := cap.Tools["dotnet"]; ok {
|
||||
addLabel("dotnet")
|
||||
}
|
||||
if _, ok := cap.Tools["java"]; ok {
|
||||
addLabel("java")
|
||||
}
|
||||
if _, ok := cap.Tools["node"]; ok {
|
||||
addLabel("node")
|
||||
}
|
||||
|
||||
// Build tool labels
|
||||
for _, tool := range cap.BuildTools {
|
||||
switch tool {
|
||||
case "msbuild":
|
||||
addLabel("msbuild")
|
||||
case "visual-studio":
|
||||
addLabel("vs2022") // or detect actual version
|
||||
case "inno-setup":
|
||||
addLabel("inno-setup")
|
||||
case "nsis":
|
||||
addLabel("nsis")
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
|
||||
// ToJSON converts capabilities to JSON string for transmission
|
||||
func (c *RunnerCapabilities) ToJSON() string {
|
||||
data, err := json.Marshal(c)
|
||||
@@ -251,12 +395,18 @@ func detectDockerCompose(ctx context.Context) bool {
|
||||
|
||||
func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
toolDetectors := map[string]func(context.Context) []string{
|
||||
"node": detectNodeVersions,
|
||||
"go": detectGoVersions,
|
||||
"python": detectPythonVersions,
|
||||
"java": detectJavaVersions,
|
||||
"dotnet": detectDotnetVersions,
|
||||
"rust": detectRustVersions,
|
||||
"node": detectNodeVersions,
|
||||
"go": detectGoVersions,
|
||||
"python": detectPythonVersions,
|
||||
"java": detectJavaVersions,
|
||||
"dotnet": detectDotnetVersions,
|
||||
"rust": detectRustVersions,
|
||||
"ruby": detectRubyVersions,
|
||||
"php": detectPHPVersions,
|
||||
"swift": detectSwiftVersions,
|
||||
"kotlin": detectKotlinVersions,
|
||||
"flutter": detectFlutterVersions,
|
||||
"dart": detectDartVersions,
|
||||
}
|
||||
|
||||
for tool, detector := range toolDetectors {
|
||||
@@ -264,6 +414,242 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
cap.Tools[tool] = versions
|
||||
}
|
||||
}
|
||||
|
||||
// Detect additional tools that just need presence check
|
||||
simpleTools := map[string]string{
|
||||
"git": "git",
|
||||
"cmake": "cmake",
|
||||
"make": "make",
|
||||
"ninja": "ninja",
|
||||
"gradle": "gradle",
|
||||
"maven": "mvn",
|
||||
"npm": "npm",
|
||||
"yarn": "yarn",
|
||||
"pnpm": "pnpm",
|
||||
"cargo": "cargo",
|
||||
"pip": "pip3",
|
||||
}
|
||||
|
||||
for name, cmd := range simpleTools {
|
||||
if v := detectSimpleToolVersion(ctx, cmd); v != "" {
|
||||
cap.Tools[name] = []string{v}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func detectBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
detectWindowsBuildTools(ctx, cap)
|
||||
case "darwin":
|
||||
detectMacOSBuildTools(ctx, cap)
|
||||
case "linux":
|
||||
detectLinuxBuildTools(ctx, cap)
|
||||
}
|
||||
}
|
||||
|
||||
func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
// Check for Visual Studio via vswhere
|
||||
vswherePaths := []string{
|
||||
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`,
|
||||
`C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe`,
|
||||
}
|
||||
for _, vswhere := range vswherePaths {
|
||||
if _, err := os.Stat(vswhere); err == nil {
|
||||
cmd := exec.CommandContext(ctx, vswhere, "-latest", "-property", "displayName")
|
||||
if output, err := cmd.Output(); err == nil && len(output) > 0 {
|
||||
cap.BuildTools = append(cap.BuildTools, "visual-studio")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MSBuild
|
||||
msbuildPaths := []string{
|
||||
`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
`C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
`C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe`,
|
||||
}
|
||||
for _, msbuild := range msbuildPaths {
|
||||
if _, err := os.Stat(msbuild); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "msbuild")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Inno Setup
|
||||
innoSetupPaths := []string{
|
||||
`C:\Program Files (x86)\Inno Setup 6\ISCC.exe`,
|
||||
`C:\Program Files\Inno Setup 6\ISCC.exe`,
|
||||
`C:\Program Files (x86)\Inno Setup 5\ISCC.exe`,
|
||||
`C:\Program Files\Inno Setup 5\ISCC.exe`,
|
||||
}
|
||||
for _, iscc := range innoSetupPaths {
|
||||
if _, err := os.Stat(iscc); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||
break
|
||||
}
|
||||
}
|
||||
// Also check PATH
|
||||
if _, err := exec.LookPath("iscc"); err == nil {
|
||||
if !contains(cap.BuildTools, "inno-setup") {
|
||||
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for NSIS
|
||||
nsisPaths := []string{
|
||||
`C:\Program Files (x86)\NSIS\makensis.exe`,
|
||||
`C:\Program Files\NSIS\makensis.exe`,
|
||||
}
|
||||
for _, nsis := range nsisPaths {
|
||||
if _, err := os.Stat(nsis); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, err := exec.LookPath("makensis"); err == nil {
|
||||
if !contains(cap.BuildTools, "nsis") {
|
||||
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for WiX Toolset
|
||||
wixPaths := []string{
|
||||
`C:\Program Files (x86)\WiX Toolset v3.11\bin\candle.exe`,
|
||||
`C:\Program Files (x86)\WiX Toolset v3.14\bin\candle.exe`,
|
||||
}
|
||||
for _, wix := range wixPaths {
|
||||
if _, err := os.Stat(wix); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "wix")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for signtool (Windows SDK)
|
||||
signtoolPaths, _ := filepath.Glob(`C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe`)
|
||||
if len(signtoolPaths) > 0 {
|
||||
cap.BuildTools = append(cap.BuildTools, "signtool")
|
||||
}
|
||||
}
|
||||
|
||||
func detectMacOSBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
// Check for xcpretty
|
||||
if _, err := exec.LookPath("xcpretty"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "xcpretty")
|
||||
}
|
||||
|
||||
// Check for fastlane
|
||||
if _, err := exec.LookPath("fastlane"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "fastlane")
|
||||
}
|
||||
|
||||
// Check for CocoaPods
|
||||
if _, err := exec.LookPath("pod"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "cocoapods")
|
||||
}
|
||||
|
||||
// Check for Carthage
|
||||
if _, err := exec.LookPath("carthage"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "carthage")
|
||||
}
|
||||
|
||||
// Check for SwiftLint
|
||||
if _, err := exec.LookPath("swiftlint"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "swiftlint")
|
||||
}
|
||||
|
||||
// Check for create-dmg or similar
|
||||
if _, err := exec.LookPath("create-dmg"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "create-dmg")
|
||||
}
|
||||
|
||||
// Check for Packages (packagesbuild)
|
||||
if _, err := exec.LookPath("packagesbuild"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "packages")
|
||||
}
|
||||
|
||||
// Check for pkgbuild (built-in)
|
||||
if _, err := exec.LookPath("pkgbuild"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "pkgbuild")
|
||||
}
|
||||
|
||||
// Check for codesign (built-in)
|
||||
if _, err := exec.LookPath("codesign"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "codesign")
|
||||
}
|
||||
|
||||
// Check for notarytool (built-in with Xcode)
|
||||
if _, err := exec.LookPath("notarytool"); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, "notarytool")
|
||||
}
|
||||
}
|
||||
|
||||
func detectLinuxBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||
// Check for common Linux build tools
|
||||
tools := []string{
|
||||
"gcc", "g++", "clang", "clang++",
|
||||
"autoconf", "automake", "libtool",
|
||||
"pkg-config", "meson",
|
||||
"dpkg-deb", "rpmbuild", "fpm",
|
||||
"appimage-builder", "linuxdeploy",
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
if _, err := exec.LookPath(tool); err == nil {
|
||||
cap.BuildTools = append(cap.BuildTools, tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func detectPackageManagers(ctx context.Context, cap *RunnerCapabilities) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := exec.LookPath("choco"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "chocolatey")
|
||||
}
|
||||
if _, err := exec.LookPath("scoop"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "scoop")
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "winget")
|
||||
}
|
||||
case "darwin":
|
||||
if _, err := exec.LookPath("brew"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "homebrew")
|
||||
}
|
||||
if _, err := exec.LookPath("port"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "macports")
|
||||
}
|
||||
case "linux":
|
||||
if _, err := exec.LookPath("apt"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "apt")
|
||||
}
|
||||
if _, err := exec.LookPath("yum"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "yum")
|
||||
}
|
||||
if _, err := exec.LookPath("dnf"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "dnf")
|
||||
}
|
||||
if _, err := exec.LookPath("pacman"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "pacman")
|
||||
}
|
||||
if _, err := exec.LookPath("zypper"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "zypper")
|
||||
}
|
||||
if _, err := exec.LookPath("apk"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "apk")
|
||||
}
|
||||
if _, err := exec.LookPath("snap"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "snap")
|
||||
}
|
||||
if _, err := exec.LookPath("flatpak"); err == nil {
|
||||
cap.PackageManagers = append(cap.PackageManagers, "flatpak")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func detectNodeVersions(ctx context.Context) []string {
|
||||
@@ -284,16 +670,8 @@ func detectPythonVersions(ctx context.Context) []string {
|
||||
|
||||
// Also try python
|
||||
if v := detectToolVersion(ctx, "python", "--version", "Python "); len(v) > 0 {
|
||||
// Avoid duplicates
|
||||
for _, ver := range v {
|
||||
found := false
|
||||
for _, existing := range versions {
|
||||
if existing == ver {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if !contains(versions, ver) {
|
||||
versions = append(versions, ver)
|
||||
}
|
||||
}
|
||||
@@ -309,20 +687,17 @@ func detectJavaVersions(ctx context.Context) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Java version output goes to stderr and looks like: openjdk version "17.0.1" or java version "1.8.0_301"
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "version") {
|
||||
// Extract version from quotes
|
||||
start := strings.Index(line, "\"")
|
||||
end := strings.LastIndex(line, "\"")
|
||||
if start != -1 && end > start {
|
||||
version := line[start+1 : end]
|
||||
// Simplify version (e.g., "17.0.1" -> "17")
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) > 0 {
|
||||
if parts[0] == "1" && len(parts) > 1 {
|
||||
return []string{parts[1]} // Java 8 style: 1.8 -> 8
|
||||
return []string{parts[1]}
|
||||
}
|
||||
return []string{parts[0]}
|
||||
}
|
||||
@@ -347,21 +722,11 @@ func detectDotnetVersions(ctx context.Context) []string {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Format: "8.0.100 [/path/to/sdk]"
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) > 0 {
|
||||
version := parts[0]
|
||||
// Simplify to major version
|
||||
major := strings.Split(version, ".")[0]
|
||||
// Avoid duplicates
|
||||
found := false
|
||||
for _, v := range versions {
|
||||
if v == major {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if !contains(versions, major) {
|
||||
versions = append(versions, major)
|
||||
}
|
||||
}
|
||||
@@ -374,6 +739,58 @@ func detectRustVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "rustc", "--version", "rustc ")
|
||||
}
|
||||
|
||||
func detectRubyVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "ruby", "--version", "ruby ")
|
||||
}
|
||||
|
||||
func detectPHPVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "php", "--version", "PHP ")
|
||||
}
|
||||
|
||||
func detectSwiftVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "swift", "--version", "Swift version ")
|
||||
}
|
||||
|
||||
func detectKotlinVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "kotlin", "-version", "Kotlin version ")
|
||||
}
|
||||
|
||||
func detectFlutterVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "flutter", "--version", "Flutter ")
|
||||
}
|
||||
|
||||
func detectDartVersions(ctx context.Context) []string {
|
||||
return detectToolVersion(ctx, "dart", "--version", "Dart SDK version: ")
|
||||
}
|
||||
|
||||
func detectSimpleToolVersion(ctx context.Context, cmd string) string {
|
||||
if _, err := exec.LookPath(cmd); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
c := exec.CommandContext(timeoutCtx, cmd, "--version")
|
||||
output, err := c.Output()
|
||||
if err != nil {
|
||||
// Try without --version for tools that don't support it
|
||||
return "installed"
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(strings.Split(string(output), "\n")[0])
|
||||
// Extract version number if possible
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
// Look for something that looks like a version
|
||||
if len(part) > 0 && (part[0] >= '0' && part[0] <= '9' || part[0] == 'v') {
|
||||
return strings.TrimPrefix(part, "v")
|
||||
}
|
||||
}
|
||||
|
||||
return "installed"
|
||||
}
|
||||
|
||||
func detectToolVersion(ctx context.Context, cmd string, args string, prefix string) []string {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -391,13 +808,10 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
|
||||
}
|
||||
}
|
||||
|
||||
// Get just the version number
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
version := parts[0]
|
||||
// Clean up version string
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
// Return major.minor or just major
|
||||
vparts := strings.Split(version, ".")
|
||||
if len(vparts) >= 2 {
|
||||
return []string{vparts[0] + "." + vparts[1]}
|
||||
@@ -407,3 +821,12 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user