getdeps/main.go (235 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
// getdeps is a tool to fetch the dependencies necessary to build OSF (Open
// System System Firmware). It works by parsing a JSON configuration containing
// the definitions of all the components that should be fetched.
// This tool also generates a JSON file containing all the components' versions,
// suitable for using in the `internal_versions` VPD variable for OSF.
//
// The supported components currently are:
// - coreboot
// - kernel (linux)
// - initramfs (u-root)
//
// For each component it is possible to specify the source (e.g. the git repo
// or package URL), its version (e.g. the branch and git commit hash, or the
// package version and hash).
//
// It is also possible to specify an URL overrides file, which will replace the
// corresponding component's URL with the override. This is useful if you want,
// for example, use alternative mirrors and repositories for a specific
// component.
//
// The hash mode allows you to be strict or permissive in the hash validation,
// and, when used in update mode, it lets you use the latest commit hashes.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strings"
flag "github.com/spf13/pflag"
)
// Flags.
var (
flagHashMode = flag.StringP("hashmode", "H", string(hashModeStrict),
"Hash verification mode: "+
"strict - require hashes for repos and blobs, check out for repos and verify for blobs; "+
"permissive - use hashes that are present but don't require; "+
"update - zero out all the hashes in the beginning, update to whatever is found.")
flagComponents = flag.StringP("components", "C", "",
fmt.Sprintf("Comma-separated list of components to fetch. If empty or not specified, fetch all of them Supported components: %s", strings.Join(supportedComponents, ", ")))
flagConfigFile = flag.StringP("config", "c", "config.json", "Configuration file")
flagURLOverridesFile = flag.StringP("url-overrides", "u", "", "URL overrides file")
flagFinalConfigFile = flag.StringP("output", "o", "", "Path to the output config file after all expansions, suitable for storing in the `internal_versions` VPD variable")
flagBaseDir = flag.StringP("basedir", "d", "", "Base directory for relative includes. If unspecified, the current working directory is used for relative includes")
)
// HashMode represents the hash mode to use. See constants below.
type HashMode string
const (
hashModeStrict HashMode = "strict"
hashModePermissive HashMode = "permissive"
hashModeUpdate HashMode = "update"
)
var (
defaultBranch = "master"
supportedComponents = []string{"initramfs", "kernel", "coreboot"}
supportedHashModes = []HashMode{hashModeStrict, hashModePermissive, hashModeUpdate}
)
// Component defines an interface for the different components
type Component interface {
Get(projectDir string, overrides *URLOverrides, hashMode HashMode) error
}
// Merge fields from src object into dst.
// - If source field is zero value, skip.
// For non-pointer fields this means they cannot be cleared.
// - If a field is a pointer field and the source points to a zero value,
// clear the corresponding dst field (set ot nil).
// - For anything else, copy the src to dst.
func mergeFields(dst reflect.Value, src reflect.Value) {
for i := 0; i < dst.NumField(); i++ {
sf, df := src.Field(i), dst.Field(i)
if sf.IsZero() {
continue
}
if sf.Kind() == reflect.Ptr {
if sf.Elem().IsZero() {
// Create a nil pointer of field's type.
nilValue := reflect.New(sf.Type()).Elem()
df.Set(nilValue)
} else {
df.Set(sf)
}
} else {
df.Set(sf)
}
}
}
// identifyRepo returns an identifier for the repository checked out at the
// specified directory, if any.
func identifyRepo(dir string) (string, error) {
// Is it a Git repo?
cmd := exec.Command("git", "describe", "--dirty", "--tags", "--always")
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err == nil {
return strings.TrimSpace(string(out)), nil
}
// is it a Hg repo?
cmd = exec.Command("hg", "identify")
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err == nil {
return strings.TrimSpace(string(out)), nil
}
return "", fmt.Errorf("not a Git or Hg repo")
}
func getBuildID(configFile string, cwd string) string {
// If running under Buck, use its working dir at the time of build invocation.
if val, ok := os.LookupEnv("BUCK_CLIENT_PWD"); ok {
if id, err := identifyRepo(val); err != nil {
return id
}
}
// Next, try directory of the config.
if absConfigFilePath, err := filepath.Abs(configFile); err == nil {
if id, err := identifyRepo(filepath.Dir(absConfigFilePath)); err == nil {
return id
}
}
// Finally, try CWD.
if id, err := identifyRepo(cwd); err == nil {
return id
}
// Give up.
return "???"
}
// expandComponent parses a comma-separated list of components, validates their
// names, and removes duplicates.
func expandComponents(componentString string) ([]string, error) {
// if no component is specified, assume all supported components.
if componentString == "" {
return supportedComponents, nil
}
components := strings.Split(componentString, ",")
if len(components) == 0 {
return supportedComponents, nil
}
cMap := make(map[string]struct{}, 0)
for _, c := range components {
found := false
c = strings.ToLower(c)
for _, sc := range supportedComponents {
if c == sc {
found = true
}
}
if found == false {
return nil, fmt.Errorf("unsupported component '%s'", c)
}
cMap[c] = struct{}{}
}
ret := make([]string, 0, len(cMap))
for k := range cMap {
ret = append(ret, k)
}
return ret, nil
}
// getBaseDir returns the absolute path of the base directory for the config files,
// given an input base dir. If the input base dir is empty, use the directory where
// the specified config file is located. This default enables config files to
// include other config files relatively to their own directory.
//
// Examples (where <PWD> means "replace with the present working directory")
//
// `basedir` is `configs`
// then `configfile` is ignored, and the returned basedir is "<PWD>/configs".
//
// `basedir` is empty, `configfile` is "configs/myconfig.json"
// then the returned basedir is <PWD>"/configs" (relative).
//
// `basedir` is empty, `configfile` is "/configs/myconfig.json"
// then the returned basedir is "/configs" (absolute).
//
// `basedir` is empty, `configfile` is `configs/`
// then the returned basedir is <PWD>"/configs".
//
// `basedir` is empty, `configfile` is empty
// then the returned basedir is <PWD>.
func getBaseDir(basedir, configfile string) (string, error) {
if basedir != "" {
return basedir, nil
}
basedir, err := filepath.Abs(filepath.Dir(configfile))
if err != nil {
return "", fmt.Errorf("failed to get absolute path for '%s': %w", filepath.Dir(configfile), err)
}
return basedir, nil
}
func main() {
flag.Parse()
baseDir, err := getBaseDir(*flagBaseDir, *flagConfigFile)
if err != nil {
log.Fatalf("Failed to get base dir: %v", err)
}
// expand requested components
components, err := expandComponents(*flagComponents)
if err != nil {
log.Fatalf("Invalid components: %v", err)
}
found := false
for _, hm := range supportedHashModes {
if *flagHashMode == string(hm) {
found = true
break
}
}
if !found {
log.Fatalf("unsupported hash mode %q", *flagHashMode)
}
configData, err := ioutil.ReadFile(*flagConfigFile)
if err != nil {
log.Fatalf("Failed to read configuration file '%s': %v", *flagConfigFile, err)
}
config, err := NewConfigWithIncludes(configData, baseDir)
if err != nil {
log.Fatalln(err)
}
// load URL overrides file
var urlOverrides *URLOverrides
if *flagURLOverridesFile != "" {
urloverridesData, err := ioutil.ReadFile(*flagURLOverridesFile)
if err != nil {
log.Fatalf("Failed to open URL overrides file '%s': %v", *flagURLOverridesFile, err)
}
urlOverrides, err = NewURLOverrides(urloverridesData)
if err != nil {
log.Fatalln(err)
}
}
projectDir, err := os.Getwd()
if err != nil {
log.Fatalln(err)
}
buildID := getBuildID(*flagConfigFile, projectDir)
for _, componentName := range components {
var component Component
switch componentName {
case "coreboot":
component = config.Coreboot
case "kernel":
component = config.Kernel
case "initramfs":
component = config.Initramfs
default:
// this should not happen, unless the switch cases are not kept in
// sync with the `supportedComponents` variable.
log.Fatalf("Unsupported component '%s'. This could be a bug, please report it to the maintainers", componentName)
}
workingDir := path.Join(projectDir, componentName)
log.Printf("Build ID: %s", buildID)
// clean up previous working directory
if err = os.RemoveAll(workingDir); err != nil {
log.Fatalln(err)
}
// create new working directory
if err = os.Mkdir(workingDir, os.ModePerm); err != nil {
log.Fatalln(err)
}
// change working directory
if err := os.Chdir(workingDir); err != nil {
log.Fatalln(err)
}
// get the sources
if err := component.Get(baseDir, urlOverrides, HashMode(*flagHashMode)); err != nil {
log.Fatalln(err)
}
}
// To ensure consistent formatting when the config is fed into vpd,
// write out a final versions file whether or not the base config was
// patched. This will also expose fields that were not explicitly set
// in hand-written config files.
// If the file already exists, override only the portion that was processed.
finalConfigFile := *flagFinalConfigFile
if finalConfigFile != "" {
if !filepath.IsAbs(finalConfigFile) {
finalConfigFile = filepath.Join(projectDir, finalConfigFile)
}
act := "Wrote"
var finalConfig *Config
finalConfigData, err := ioutil.ReadFile(finalConfigFile)
if err == nil {
if fc, err := NewConfig(finalConfigData); err == nil {
finalConfig = fc
act = "Updated"
}
}
if finalConfig == nil {
finalConfig = &Config{}
}
finalConfig.BuildID = buildID
for _, componentName := range components {
switch componentName {
case "coreboot":
finalConfig.Coreboot = config.Coreboot
case "kernel":
finalConfig.Kernel = config.Kernel
case "initramfs":
finalConfig.Initramfs = config.Initramfs
}
}
indentedConfig, err := json.MarshalIndent(finalConfig, "", " ")
if err != nil {
log.Fatalf("Failed to marshal configuration: %v", err)
}
if err := ioutil.WriteFile(finalConfigFile, indentedConfig, 0644); err != nil {
log.Fatalf("Failed to write generated versions to file '%s': %v", finalConfigFile, err)
}
log.Printf("%s %s", act, finalConfigFile)
}
}