Open-CMSIS-Pack/cbuild

View on GitHub
pkg/utils/utils.go

Summary

Maintainability
A
0 mins
Test Coverage
C
77%
/*
 * Copyright (c) 2022-2024 Arm Limited. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package utils

import (
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"
    "time"

    "github.com/Open-CMSIS-Pack/cbuild/v2/pkg/errutils"
    log "github.com/sirupsen/logrus"
    "gopkg.in/yaml.v3"
)

type EnvVars struct {
    PackRoot     string
    CompilerRoot string
    BuildRoot    string
}

type ContextItem struct {
    ProjectName string
    BuildType   string
    TargetType  string
}

func GetExecutablePath() (string, error) {
    exec, err := os.Executable()
    if err != nil {
        return "", err
    }
    execReal, err := filepath.EvalSymlinks(exec)
    if err != nil {
        return "", err
    }
    executablePath := filepath.Dir(execReal)
    return executablePath, nil
}

func UpdateEnvVars(binPath string, etcPath string) (env EnvVars) {
    env.PackRoot = os.Getenv("CMSIS_PACK_ROOT")
    if env.PackRoot == "" {
        packRoot := GetDefaultCmsisPackRoot()
        if packRoot != "" {
            env.PackRoot, _ = filepath.Abs(packRoot)
            os.Setenv("CMSIS_PACK_ROOT", env.PackRoot)
        }
    }
    env.CompilerRoot = os.Getenv("CMSIS_COMPILER_ROOT")
    if env.CompilerRoot == "" {
        env.CompilerRoot, _ = filepath.Abs(etcPath)
        os.Setenv("CMSIS_COMPILER_ROOT", env.CompilerRoot)
    }
    env.BuildRoot, _ = filepath.Abs(binPath)
    log.Debug("CMSIS_PACK_ROOT: " + env.PackRoot)
    log.Debug("CMSIS_COMPILER_ROOT: " + env.CompilerRoot)
    return env
}

func GetDefaultCmsisPackRoot() (root string) {
    if runtime.GOOS == "windows" {
        root = os.Getenv("LOCALAPPDATA")
        if root == "" {
            root = os.Getenv("USERPROFILE")
            if root != "" {
                root = root + "\\AppData\\Local"
            }
        }
        if root != "" {
            root = root + "\\Arm\\Packs"
        }
    } else {
        root = os.Getenv("XDG_CACHE_HOME")
        if root == "" {
            root = os.Getenv("HOME")
            if root != "" {
                root = root + "/.cache"
            }
        }
        if root != "" {
            root = root + "/arm/packs"
        }
    }
    return filepath.Clean(root)
}

func ParseContext(context string) (item ContextItem, err error) {
    parseError := errutils.New(errutils.ErrInvalidContextFormat)

    periodCount := strings.Count(context, ".")
    plusCount := strings.Count(context, "+")
    if context == "" || periodCount > 1 || plusCount > 1 {
        err = parseError
        return
    }

    var projectName, buildType, targetType string

    targetIdx := strings.Index(context, "+")
    buildIdx := strings.Index(context, ".")

    if (targetIdx != -1 && buildIdx != -1) && targetIdx < buildIdx {
        err = parseError
        return
    }

    if targetIdx == -1 && buildIdx == -1 {
        projectName = context
    } else if buildIdx == -1 {
        // context with only projectName+targetType
        projectName = context[:targetIdx]
        targetType = context[targetIdx+1:]
    } else if targetIdx == -1 {
        // context with only projectName.buildtype
        projectName = context[:buildIdx]
        buildType = context[buildIdx+1:]
    } else {
        // fully specified contexts
        part := context[:targetIdx]
        buildIdx := strings.Index(part, ".")

        if buildIdx > -1 {
            projectName = part[:buildIdx]
            buildType = part[buildIdx+1:]
        } else {
            projectName = part
        }

        part = context[targetIdx+1:]
        buildIdx = strings.Index(part, ".")

        if buildIdx > -1 {
            targetType = part[:buildIdx]
            buildType = part[buildIdx+1:]
        } else {
            targetType = part
        }
    }

    item.ProjectName = projectName
    item.BuildType = buildType
    item.TargetType = targetType
    return
}

func CreateContext(contextItem ContextItem) (context string) {
    context = contextItem.ProjectName
    if contextItem.BuildType != "" {
        context += "." + contextItem.BuildType
    }
    if contextItem.TargetType != "" {
        context += "+" + contextItem.TargetType
    }
    return
}

type CbuildIndex struct {
    BuildIdx struct {
        GeneratedBy string `yaml:"generated-by"`
        Cdefault    string `yaml:"cdefault"`
        Csolution   string `yaml:"csolution"`
        TmpDir      string `yaml:"tmpdir"`
        Cprojects   []struct {
            Cproject string `yaml:"cproject"`
        } `yaml:"cprojects"`
        Licenses interface{} `yaml:"licenses"`
        Cbuilds  []struct {
            Cbuild        string `yaml:"cbuild"`
            Project       string `yaml:"project"`
            Configuration string `yaml:"configuration"`
            Rebuild       bool   `yaml:"rebuild"`
        } `yaml:"cbuilds"`
        Executes []interface{} `yaml:"executes"`
        Rebuild  bool          `yaml:"rebuild"`
    } `yaml:"build-idx"`
}

type CbuildSet struct {
    ContextSet struct {
        GeneratedBy string `yaml:"generated-by"`
        Contexts    []struct {
            Context string `yaml:"context"`
        } `yaml:"contexts"`
        Compiler string `yaml:"compiler"`
    } `yaml:"cbuild-set"`
}

func ParseCbuildIndexFile(cbuildIndexFile string) (data CbuildIndex, err error) {
    yfile, err := os.ReadFile(cbuildIndexFile)
    if err != nil {
        return
    }
    err = yaml.Unmarshal(yfile, &data)
    return
}

func ParseCbuildSetFile(cbuildSetFile string) (data CbuildSet, err error) {
    yfile, err := os.ReadFile(cbuildSetFile)
    if err != nil {
        return
    }
    err = yaml.Unmarshal(yfile, &data)
    return
}

func AppendUnique[T comparable](slice []T, elems ...T) []T {
    lookup := make(map[T]struct{})
    all := append(slice, elems...)
    var unique []T
    for _, elem := range all {
        _, isDuplicate := lookup[elem]
        if !isDuplicate {
            lookup[elem] = struct{}{}
            unique = append(unique, elem)
        }
    }
    return unique
}

func Contains[T comparable](slice []T, elem T) bool {
    for _, sliceElem := range slice {
        if sliceElem == elem {
            return true
        }
    }
    return false
}

func GetInstalledExePath(exeName string) (path string, err error) {
    path, err = exec.LookPath(exeName)
    if err != nil {
        path = NormalizePath(path)
    }
    return
}

func NormalizePath(path string) string {
    if strings.Contains(path, "\\") {
        path = strings.ReplaceAll(path, "\\", "/")
    }
    return path
}

func ResolveContexts(allContext []string, contextFilters []string) ([]string, error) {
    var selectedContexts []string

    // remove duplicates (if any)
    filters := RemoveDuplicates(contextFilters)

    for _, filter := range filters {
        filterContextItem, err := ParseContext(filter)
        if err != nil {
            return nil, err
        }
        matchFound := false
        for _, context := range allContext {
            availableContextItem, err := ParseContext(context)
            if err != nil {
                return nil, err
            }

            var contextPattern string
            if filterContextItem.ProjectName != "" {
                contextPattern = filterContextItem.ProjectName
            } else {
                contextPattern = "*"
            }

            contextPattern += "."
            if filterContextItem.BuildType != "" {
                contextPattern += filterContextItem.BuildType
            } else {
                contextPattern += "*"
            }

            contextPattern += "+"
            if filterContextItem.TargetType != "" {
                contextPattern += filterContextItem.TargetType
            } else {
                contextPattern += "*"
            }

            fullContextItem := availableContextItem.ProjectName + "." + availableContextItem.BuildType + "+" + availableContextItem.TargetType

            match, err := MatchString(fullContextItem, contextPattern)
            if err != nil {
                return nil, err
            }
            if match && !Contains(selectedContexts, context) {
                matchFound = match
                selectedContexts = append(selectedContexts, context)
            }
        }
        if !matchFound {
            return nil, errutils.New(errutils.ErrNoFilteredContextFound, filter)
        }
    }
    return selectedContexts, nil
}

func LogStdMsg(msg string) {
    if msg != "" {
        _, _ = log.StandardLogger().Out.Write([]byte(msg + "\n"))
    }
}

func FormatTime(time time.Duration) string {
    // Format time in "hh:mm:ss"
    return fmt.Sprintf("%02d:%02d:%02d", int(time.Hours()), int(time.Minutes())%60, int(time.Seconds())%60)
}

func RemoveDuplicates(input []string) []string {
    // Create a map to track seen strings
    seen := make(map[string]bool)
    // Create a slice to store the unique strings
    var result []string

    // Iterate over the input slice
    for _, str := range input {
        // If the string is not in the map,
        // add it to the result and mark it as seen
        if !seen[str] {
            result = append(result, str)
            seen[str] = true
        }
    }

    return result
}

func FileExists(filePath string) (bool, error) {
    _, err := os.Stat(filePath)
    if err == nil {
        // File exists
        return true, nil
    }
    if os.IsNotExist(err) {
        // File doesn't exist
        return false, errutils.New(errutils.ErrFileNotExist, filePath)
    }
    // Return error for any other issues (permission denied, etc.)
    return false, err
}

func PrintSeparator(delimiter string, length int) {
    if length > 0 {
        sep := strings.Repeat(delimiter, length-1)
        LogStdMsg("+" + sep)
    }
}

// checks if two paths are equivalent
func ComparePaths(path1, path2 string) (bool, error) {
    cleanPath1 := filepath.Clean(path1)
    cleanPath2 := filepath.Clean(path2)

    absPath1, err := filepath.Abs(cleanPath1)
    if err != nil {
        return false, err
    }
    absPath2, err := filepath.Abs(cleanPath2)
    if err != nil {
        return false, err
    }

    if isFileSystemCaseInsensitive() {
        absPath1 = strings.ToLower(absPath1)
        absPath2 = strings.ToLower(absPath2)
    }

    return absPath1 == absPath2, nil
}

func isFileSystemCaseInsensitive() bool {
    // On Windows and macOS, file systems are typically case insensitive
    // On Linux, file systems are typically case sensitive
    return filepath.Separator == '\\' || strings.Contains(strings.ToLower(os.Getenv("OS")), "darwin")
}