github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/sysinfo/sysinfo_darwin.go (about)

     1  package sysinfo
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"os"
     9  	"os/exec"
    10  	"regexp"
    11  	"strconv"
    12  
    13  	"github.com/ActiveState/cli/internal/errs"
    14  	"github.com/ActiveState/cli/internal/fileutils"
    15  	"github.com/ActiveState/cli/internal/locale"
    16  	"github.com/ActiveState/cli/internal/logging"
    17  	"github.com/patrickmn/go-cache"
    18  )
    19  
    20  // OS returns the system's OS
    21  func OS() OsInfo {
    22  	return Mac
    23  }
    24  
    25  var (
    26  	plistVersionRegex = regexp.MustCompile(`(?s)ProductVersion.*?([\d\.]+)`)
    27  )
    28  
    29  // OSVersion returns the system's OS version.
    30  func OSVersion() (*OSVersionInfo, error) {
    31  	if cached, found := sysinfoCache.Get(osVersionInfoCacheKey); found {
    32  		return cached.(*OSVersionInfo), nil
    33  	}
    34  
    35  	if v := os.Getenv(VersionOverrideEnvVar); v != "" {
    36  		vInfo, err := parseVersionInfo(v)
    37  		if err != nil {
    38  			return nil, fmt.Errorf("Could not parse version info: %w", err)
    39  		}
    40  		return &OSVersionInfo{vInfo, "spoofed"}, nil
    41  	}
    42  
    43  	// Fetch OS version.
    44  	version, err := getDarwinProductVersion()
    45  	if err != nil {
    46  		return nil, fmt.Errorf("Unable to determine OS version: %v", err)
    47  	}
    48  
    49  	vInfo, err := parseVersionInfo(version)
    50  	if err != nil {
    51  		return nil, fmt.Errorf("Unable to parse OS version: %w", err)
    52  	}
    53  
    54  	// Fetch OS name.
    55  	name, err := exec.Command("sw_vers", "-productName").Output()
    56  	if err != nil {
    57  		return nil, fmt.Errorf("Unable to fetch OS name: %s", err)
    58  	}
    59  
    60  	info := &OSVersionInfo{vInfo, string(name)}
    61  	sysinfoCache.Set(osVersionInfoCacheKey, info, cache.NoExpiration)
    62  	return info, nil
    63  }
    64  
    65  // Libc returns the system's C library.
    66  func Libc() (*LibcInfo, error) {
    67  	if cached, found := sysinfoCache.Get(libcInfoCacheKey); found {
    68  		return cached.(*LibcInfo), nil
    69  	}
    70  
    71  	version, err := exec.Command("clang", "--version").Output()
    72  	if err != nil {
    73  		return nil, fmt.Errorf("Unable to fetch libc version: %s", err)
    74  	}
    75  	regex := regexp.MustCompile(`(\d+)\D(\d+)`)
    76  	parts := regex.FindStringSubmatch(string(version))
    77  	if len(parts) != 3 {
    78  		return nil, fmt.Errorf("Unable to parse libc string '%s'", version)
    79  	}
    80  	for i := 1; i < len(parts); i++ {
    81  		if _, err := strconv.Atoi(parts[i]); err != nil {
    82  			return nil, fmt.Errorf("Unable to parse part '%s' of libc string '%s'", parts[i], version)
    83  		}
    84  	}
    85  	major, _ := strconv.Atoi(parts[1])
    86  	minor, _ := strconv.Atoi(parts[2])
    87  	info := &LibcInfo{BsdLibc, major, minor}
    88  	sysinfoCache.Set(libcInfoCacheKey, info, cache.NoExpiration)
    89  	return info, nil
    90  }
    91  
    92  // Compilers returns the system's available compilers.
    93  func Compilers() ([]*CompilerInfo, error) {
    94  	if cached, found := sysinfoCache.Get(compilersCacheKey); found {
    95  		return cached.([]*CompilerInfo), nil
    96  	}
    97  
    98  	compilers := []*CompilerInfo{}
    99  
   100  	// Map of compiler commands to CompilerNameInfos.
   101  	var compilerMap = map[string]CompilerNameInfo{
   102  		"clang": Clang,
   103  	}
   104  	for command, nameInfo := range compilerMap {
   105  		major, minor, err := getCompilerVersion([]string{command, "--version"})
   106  		if err != nil {
   107  			return compilers, err
   108  		} else if major > 0 {
   109  			compilers = append(compilers, &CompilerInfo{nameInfo, major, minor})
   110  		}
   111  	}
   112  
   113  	sysinfoCache.Set(compilersCacheKey, compilers, cache.NoExpiration)
   114  	return compilers, nil
   115  }
   116  
   117  func getDarwinProductVersion() (string, error) {
   118  	v, err := getDarwinProductVersionFromFS()
   119  	if err != nil && !errors.Is(err, fs.ErrNotExist) {
   120  		logging.Warning("Unable to fetch OS version from filesystem: %v", errs.JoinMessage(err))
   121  	} else if err == nil {
   122  		return v, nil
   123  	}
   124  
   125  	version, err := exec.Command("sw_vers", "-productVersion").Output()
   126  	if err != nil {
   127  		return "", locale.WrapError(err, "Could not detect your OS version, error received: %s", err.Error())
   128  	}
   129  	return string(bytes.TrimSpace(version)), nil
   130  }
   131  
   132  func getDarwinProductVersionFromFS() (string, error) {
   133  	fpath := "/System/Library/CoreServices/SystemVersion.plist"
   134  	if !fileutils.TargetExists(fpath) {
   135  		return "", fs.ErrNotExist
   136  	}
   137  
   138  	b, err := fileutils.ReadFile(fpath)
   139  	if err != nil {
   140  		return "", errs.Wrap(err, "Could not read %s", fpath)
   141  	}
   142  
   143  	match := plistVersionRegex.FindSubmatch(b)
   144  	if len(match) != 2 {
   145  		return "", errs.Wrap(err, "SystemVersion.plist does not contain a valid ProductVersion, match result: %v, xml:\n%s", match, string(b))
   146  	}
   147  
   148  	return string(match[1]), nil
   149  }