blob: 4e88babbba05895fc742655db5c98b828cb4ae65 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package profiles
import (
"fmt"
"os"
"runtime"
"sort"
"strconv"
"strings"
"time"
"v.io/x/lib/envvar"
)
// Target represents specification for the environment that the profile is
// to be built for. Targets include a version string to allow for upgrades and
// for the simultaneous existence of incompatible versions.
//
// Target and Environment implement flag.Getter so that they may be used
// with the flag package. Two flags are required, one to specify the target
// in <arch>-<os>@<version> format and a second to specify environment
// variables either as comma separated values or as repeated arguments.
type Target struct {
arch, opsys, version string
// The environment as specified on the command line
commandLineEnv Environment
// The environment as modified by a profile implementation
Env Environment
InstallationDir string // where this target is installed.
UpdateTime time.Time
isSet bool
}
// Arch returns the archiecture of this target.
func (pt *Target) Arch() string {
return pt.arch
}
// OS returns the operating system of this target.
func (pt *Target) OS() string {
return pt.opsys
}
// Version returns the version of this target.
func (pt *Target) Version() string {
return pt.version
}
// SetVersion sets the version for the target.
func (pt *Target) SetVersion(v string) {
pt.version = v
}
// CommandLineEnv returns the environment variables set on the
// command line for this target.
func (pt Target) CommandLineEnv() Environment {
r := Environment{Vars: make([]string, len(pt.commandLineEnv.Vars))}
copy(r.Vars, pt.commandLineEnv.Vars)
return r
}
// UseCommandLineEnv copies the command line supplied environment variables
// into the mutable environment of the Target. It should be called as soon
// as all command line parsing has been completed and before the target is
// otherwise used.
func (pt *Target) UseCommandLineEnv() {
pt.Env = pt.CommandLineEnv()
}
// TargetSpecificDirname returns a directory name that is specific
// to that target taking account the architecture, operating system and
// command line environment variables, if relevant, into account (e.g
// GOARM={5,6,7}).
func (pt *Target) TargetSpecificDirname() string {
env := envvar.SliceToMap(pt.commandLineEnv.Vars)
dir := pt.arch + "_" + pt.opsys
if pt.arch == "arm" {
if armv, present := env["GOARM"]; present {
dir += "_armv" + armv
}
}
return dir
}
type Environment struct {
Vars []string `xml:"var"`
}
// NewTarget creates a new target using the supplied target and environment
// parameters specified in command line format.
func NewTarget(target string, env ...string) (Target, error) {
t := &Target{}
err := t.Set(target)
for _, e := range env {
t.commandLineEnv.Set(e)
}
return *t, err
}
// Match returns true if pt and pt2 meet the following criteria in the
// order they are listed:
// - if the Arch and OS fields are exactly the same
// - if pt has a non-zero length Version field, then it must be
// the same as that in pt2
// Match is used by the various methods and functions in this package
// when looking up Targets unless otherwise specified.
func (pt Target) Match(pt2 *Target) bool {
if pt.arch != pt2.arch || pt.opsys != pt2.opsys {
return false
}
if (len(pt.version) > 0) && (pt.version != pt2.version) {
return false
}
return true
}
// Less returns true if pt2 is considered less than pt. The ordering
// takes into account only the architecture, operating system and version of
// the target. The architecture and operating system are ordered
// lexicographically in ascending order, then the version is ordered but in
// descending lexicographic order except that the empty string is considered
// the 'highest' value.
// Thus, (targets in <arch>-<os>[@<version>] format), are all true:
// b-c < c-c
// b-c@3 < b-c@2
func (pt *Target) Less(pt2 *Target) bool {
switch {
case pt.arch != pt2.arch:
return pt.arch < pt2.arch
case pt.opsys != pt2.opsys:
return pt.opsys < pt2.opsys
case len(pt.version) == 0 && len(pt2.version) > 0:
return true
case len(pt.version) > 0 && len(pt2.version) == 0:
return false
case pt.version != pt2.version:
return compareVersions(pt.version, pt2.version) > 0
default:
return false
}
}
// CrossCompiling returns true if the target differs from that of the runtime.
func (pt Target) CrossCompiling() bool {
arch, _ := goarch()
return (pt.arch != arch) || (pt.opsys != runtime.GOOS)
}
// Usage returns the usage string for Target.
func (pt *Target) Usage() string {
return "specifies a profile target in the following form: <arch>-<os>[@<version>]"
}
// Set implements flag.Value.
func (t *Target) Set(val string) error {
index := strings.IndexByte(val, '@')
if index > -1 {
t.version = val[index+1:]
val = val[:index]
}
parts := strings.Split(val, "-")
if len(parts) != 2 || (len(parts[0]) == 0 || len(parts[1]) == 0) {
return fmt.Errorf("%q doesn't look like <arch>-<os>[@<version>]", val)
}
t.arch = parts[0]
t.opsys = parts[1]
t.isSet = true
return nil
}
// Get implements flag.Getter.
func (t Target) Get() interface{} {
if !t.isSet {
// Default value.
arch, isSet := goarch()
return Target{
isSet: isSet,
arch: arch,
opsys: runtime.GOOS,
version: t.version,
Env: t.Env,
}
}
return t
}
func goarch() (string, bool) {
// GOARCH may be set to 386 for binaries compiled for amd64 - i.e.
// the same binary can be run in these two modes, but the compiled
// in value of runtime.GOARCH will only ever be the value that it
// was compiled with.
if a := os.Getenv("GOARCH"); len(a) > 0 {
return a, true
}
return runtime.GOARCH, false
}
// DefaultTarget returns a default value for a Target. Use this function to
// initialize Targets that are expected to set from the command line via
// the flags package.
func DefaultTarget() Target {
arch, isSet := goarch()
return Target{
isSet: isSet,
arch: arch,
opsys: runtime.GOOS,
}
}
// NativeTarget returns a value for Target for the host on which it is running.
// Use this function for Target values that are passed into other functions
// and libraries where a native target is specifically required.
func NativeTarget() Target {
arch, _ := goarch()
return Target{
isSet: true,
arch: arch,
opsys: runtime.GOOS,
}
}
// IsSet returns true if this target has had its value set.
func (pt Target) IsSet() bool {
return pt.isSet
}
// String implements flag.Getter.
func (pt Target) String() string {
v := pt.Get().(Target)
return fmt.Sprintf("%v-%v@%s", v.arch, v.opsys, v.version)
}
// Targets is a list of *Target's ordered by architecture,
// operating system and descending versions.
type Targets []*Target
// Implements sort.Len
func (tl Targets) Len() int {
return len(tl)
}
// Implements sort.Less
func (tl Targets) Less(i, j int) bool {
return tl[i].Less(tl[j])
}
// Implements sort.Swap
func (tl Targets) Swap(i, j int) {
tl[i], tl[j] = tl[i], tl[j]
}
func (tl Targets) Sort() {
sort.Sort(tl)
}
// DebugString returns a pretty-printed representation of pt.
func (pt Target) DebugString() string {
v := pt.Get().(Target)
return fmt.Sprintf("%v-%v@%s dir:%s --env=%s envvars:%v", v.arch, v.opsys, v.version, pt.InstallationDir, strings.Join(pt.commandLineEnv.Vars, ","), pt.Env.Vars)
}
// Set implements flag.Getter.
func (e *Environment) Get() interface{} {
return *e
}
// Set implements flag.Value.
func (e *Environment) Set(val string) error {
for _, v := range strings.Split(val, ",") {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 || (len(parts[0]) == 0) {
return fmt.Errorf("%q doesn't look like var=[val]", v)
}
e.Vars = append(e.Vars, v)
}
return nil
}
// String implements flag.Getter.
func (e Environment) String() string {
return strings.Join(e.Vars, ",")
}
// Usage returns the usage string for Environment.
func (e Environment) Usage() string {
return "specify an environment variable in the form: <var>=[<val>],..."
}
// InsertTarget inserts the given target into Targets if it's not
// already there and returns a new slice.
func InsertTarget(targets Targets, target *Target) Targets {
for i, t := range targets {
if !t.Less(target) {
targets = append(targets, nil)
copy(targets[i+1:], targets[i:])
targets[i] = target
return targets
}
}
return append(targets, target)
}
// RemoveTarget removes the given target from a slice of Target and returns
// a slice.
func RemoveTarget(targets Targets, target *Target) Targets {
for i, t := range targets {
if target.Match(t) {
targets, targets[len(targets)-1] = append(targets[:i], targets[i+1:]...), nil
return targets
}
}
return targets
}
// FindTarget returns the first target that matches the requested target from
// the slice of Targets. If target has not been explicitly set and there is
// only a single target available in targets then that one target is considered
// as matching.
func FindTarget(targets Targets, target *Target) *Target {
for _, t := range targets {
if target.Match(t) {
tmp := *t
return &tmp
}
}
return nil
}
// FindTargetWithDefault is like FindTarget except that if there is only one
// target in the slice and the requested target has not been explicitly set
// (IsSet is false) then that one target is returned by default.
func FindTargetWithDefault(targets Targets, target *Target) *Target {
if len(targets) == 1 && !target.IsSet() {
tmp := *targets[0]
return &tmp
}
return FindTarget(targets, target)
}
// compareVersions compares version numbers. It handles cases like:
// compareVersions("2", "11") => 1
// compareVersions("1.1", "1.2") => 1
// compareVersions("1.2", "1.2.1") => 1
// compareVersions("1.2.1.b", "1.2.1.c") => 1
func compareVersions(v1, v2 string) int {
v1parts := strings.Split(v1, ".")
v2parts := strings.Split(v2, ".")
maxLen := len(v1parts)
if len(v2parts) > maxLen {
maxLen = len(v2parts)
}
for i := 0; i < maxLen; i++ {
if i == len(v1parts) {
// v2 has more parts than v1, so v2 > v1.
return -1
}
if i == len(v2parts) {
// v1 has more parts than v2, so v1 > v2.
return 1
}
mustCompareStrings := false
v1part, err := strconv.Atoi(v1parts[i])
if err != nil {
mustCompareStrings = true
}
v2part, err := strconv.Atoi(v2parts[i])
if err != nil {
mustCompareStrings = true
}
if mustCompareStrings {
return strings.Compare(v1parts[i], v2parts[i])
}
if v1part > v2part {
return 1
}
if v2part > v1part {
return -1
}
}
return 0
}