blob: 4bb006a844dbfffb1ba4f10fc1afd1ebb5cd20e6 [file] [log] [blame]
package dkms
import (
"context"
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"cloud.google.com/go/storage"
"cos.googlesource.com/cos/tools.git/src/pkg/cos"
"cos.googlesource.com/cos/tools.git/src/pkg/fs"
"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
"cos.googlesource.com/cos/tools.git/src/pkg/utils"
"github.com/golang/glog"
"google.golang.org/api/option"
)
// Build compiles a DKMS package using the MAKE command from dkms.conf,
// signs the compiled modules, and saves the results to the package's
// build directory in the DKMS tree.
//
// If the path to a private key and a certificate is specified in the environmental
// variables 'MODULES_SIGN_KEY' and 'MODULES_SIGN_CERT' respectively, or the files
// exist in '/var/lib/dkms/mok.key' and '/var/lib/dkms/mok.pub', then this would
// sign the compiled modules with the provided key and certificate.
//
// If the package is not already added to the DKMS source tree, this will try to
// add it.
//
// If options.InstallBuildDependencies is true, this will install the
// appropriate compiler toolchain and kernel headers for the kernel specified by
// the options fields.
//
// If any patches should be applied before building the package, as specified in
// dkms.conf, this will apply them.
//
// If options.MakeVariables is set to "cos-default", this will apply the values
// used for compiling the COS kernel and append them to the MAKE command. See
// DefaultMakeVariables for more detail.
func Build(pkg *Package, options *Options) error {
if IsBuilt(pkg) {
if !options.Force {
glog.Info("package already built; skipping build")
return nil
}
if err := Unbuild(pkg); err != nil {
return fmt.Errorf("failed to unbuild package before forcing rebuild: %v", err)
}
}
if err := Add(pkg, options); err != nil {
return err
}
glog.Infof("building package %s-%s", pkg.Name, pkg.Version)
config, err := LoadConfig(pkg)
if err != nil {
return err
}
if config.PreBuild != "" {
if err := utils.RunCommandString(pkg.BuildDir(), config.PreBuild); err != nil {
return fmt.Errorf("failed to run pre-build script: %v", err)
}
}
if options.InstallBuildDependencies {
if err := InstallBuildDependencies(pkg); err != nil {
return err
}
}
sourceDir := pkg.SourceDir()
buildDir := pkg.BuildDir()
glog.Infof("copying sources from %s to build dir %s", sourceDir, buildDir)
if err := fs.CopyDir(sourceDir, buildDir, 0777); err != nil {
return err
}
for _, patch := range config.Patches {
patchPath := path.Join(buildDir, "patches", patch)
if err := ApplyPatch(buildDir, patchPath); err != nil {
return fmt.Errorf("could not apply patch: %v", err)
}
}
makeVariables := options.MakeVariables
if makeVariables == "cos-default" {
makeVariables, err = DefaultMakeVariables(pkg)
if err != nil {
return err
}
}
makeCommand := fmt.Sprintf("%s %s", config.MakeCommand, makeVariables)
makeCommand = strings.Trim(makeCommand, " ")
if err := utils.RunCommandString(buildDir, makeCommand); err != nil {
return fmt.Errorf("failed to compile modules: %v", err)
}
if err := SignModules(config.Modules, options.PrivateKeyPath, options.CertificatePath, options.Hash); err != nil {
glog.Warningf("skipping module signing for package (%s): %v", pkg.Name, err)
}
if config.PostBuild != "" {
if err := utils.RunCommandString(buildDir, config.PostBuild); err != nil {
return fmt.Errorf("failed to run post-build script: %v", err)
}
}
return nil
}
// DefaultMakeVariables returns the default make variables for a package.
//
// The values of CC and CXX are expected to be provided by sourcing the toolchain_env
// file in the kernel source tree. The values set in toolchain_env take precedence over
// the default CC and CXX values.
//
// The default values are:
//
// ARCH=${arch}
// CC=toolchain/bin/${arch}-cros-linux-gnu-clang
// CXX=toolchain/bin/${arch}-cros-linux-gnu-clang++
// LD=toolchain/bin/${arch}-cros-linux-gnu-ld.lld
// STRIP=toolchain/bin/llvm-strip
// OBJCOPY=toolchain/bin/llvm-objcopy
// HOSTCC=${arch}-pc-linux-gnu-clang
// HOSTCXX=${arch}-pc-linux-gnu-clang++
// HOSTLD=${arch}-pc-linux-gnu-clang
func DefaultMakeVariables(pkg *Package) (string, error) {
makeVariablesMap := map[string]string{
// paths are relative to the kernel source tree
"ARCH": pkg.Arch,
"CC": fmt.Sprintf("toolchain/bin/%s-cros-linux-gnu-clang", pkg.Arch),
"CXX": fmt.Sprintf("toolchain/bin/%s-cros-linux-gnu-clang++", pkg.Arch),
"LD": fmt.Sprintf("toolchain/bin/%s-cros-linux-gnu-ld.lld", pkg.Arch),
"STRIP": "toolchain/bin/llvm-strip",
"OBJCOPY": "toolchain/bin/llvm-objcopy",
"HOSTCC": fmt.Sprintf("%s-pc-linux-gnu-clang", pkg.Arch),
"HOSTCXX": fmt.Sprintf("%s-pc-linux-gnu-clang++", pkg.Arch),
"HOSTLD": fmt.Sprintf("%s-pc-linux-gnu-clang", pkg.Arch),
}
toolchainEnvPath := path.Join(pkg.Trees.Kernel, "toolchain_env")
toolchainEnv, err := utils.SourceFile(toolchainEnvPath)
if err != nil {
return "", fmt.Errorf("could not source toolchain_env to determine build toolchain: %v", err)
}
var makeVariablesList []string
for key, value := range makeVariablesMap {
toolchainValue, ok := toolchainEnv[key]
if ok {
value = fmt.Sprintf("toolchain/bin/%s", toolchainValue)
}
makeVariablesList = append(makeVariablesList, fmt.Sprintf("%s=%s", key, value))
}
sort.Strings(makeVariablesList)
return strings.Join(makeVariablesList, " "), nil
}
// ApplyPatch applies a patch in p1 format to the files in a directory.
func ApplyPatch(dir, patchPath string) error {
file, err := os.Open(patchPath)
if err != nil {
return fmt.Errorf("could not open patch file: %v", err)
}
cmd := exec.Command("patch", "-p1")
cmd.Dir = dir
cmd.Stdin = file
out, err := cmd.CombinedOutput()
if err == nil {
glog.Info(string(out))
} else {
glog.Error(string(out))
return err
}
return nil
}
// InstallBuildDependencies downloads and installs the kernel headers and
// compiler toolchain for a package, if they are not already present.
func InstallBuildDependencies(pkg *Package) error {
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithoutAuthentication())
if err != nil {
return err
}
// Artifact paths look like gs://cos-tools/18244.151.84/lakitu/${artifact-path}
prefix := path.Join(pkg.BuildId, pkg.Board)
downloader := cos.NewGCSDownloader(client, nil, "", prefix, "", "")
return installBuildDependencies(ctx, downloader, pkg.Trees.Kernel)
}
func installBuildDependencies(ctx context.Context, downloader cos.ArtifactsDownloader, dstDir string) error {
toolchainInstalled := CompilerToolchainInstalled(dstDir)
headersInstalled := KernelHeadersInstalled(dstDir)
if !toolchainInstalled || !headersInstalled {
if err := os.MkdirAll(dstDir, 0777); err != nil {
return err
}
if !toolchainInstalled {
glog.Infof("installing compiler toolchain")
if err := InstallCompilerToolchain(ctx, downloader, dstDir); err != nil {
return err
}
glog.Infof("done installing compiler toolchain")
}
if !headersInstalled {
glog.Infof("installing kernel headers")
if err := InstallKernelHeaders(ctx, downloader, dstDir); err != nil {
return err
}
glog.Infof("done installing kernel headers")
}
}
return nil
}
// CachedBuild tries to download all of a package's built modules from the cache,
// and falls back to building them locally if they are not present in the cache.
//
// If options.Upload is specified and the package's built modules are not present
// in the cache, then this will upload them after they are built.
//
// See Build for more information on how modules are built locally.
func CachedBuild(ctx context.Context, pkg *Package, cache *gcs.GCSBucket, options *Options) error {
err := CachedAdd(ctx, pkg, cache, options)
if err != nil {
return err
}
config, err := LoadConfig(pkg)
if err != nil {
return err
}
// Get the list of modules which are not available locally
var moduleDownloads []gcs.ObjectDownload
for _, module := range config.Modules {
builtPath := module.BuiltPath()
if !fs.IsFile(builtPath) {
glog.Infof("could not find compiled module %s locally", module.BuiltName)
cacheBuiltPath := module.CacheBuiltPath()
download := gcs.NewObjectDownload(cacheBuiltPath, builtPath)
moduleDownloads = append(moduleDownloads, download)
}
}
if len(moduleDownloads) > 0 {
glog.Info("downloading missing modules from cache")
if options.DownloadWorkers == 1 {
err = cache.DownloadObjects(ctx, moduleDownloads)
} else {
err = cache.DownloadObjectsParallel(ctx, moduleDownloads, options.DownloadWorkers)
}
if err != nil {
glog.Infof("error while downloading modules: %v", err)
glog.Info("could not download some modules; building from scratch")
}
}
// If all modules were downloaded successfully, this should be a no-op.
err = Build(pkg, options)
if err != nil {
return err
}
// Upload the modules to the cache after they have all been built.
if options.Upload && !IsBuiltInCache(ctx, pkg, cache) {
for _, module := range config.Modules {
builtPath := module.BuiltPath()
cacheBuiltPath := module.CacheBuiltPath()
err := cache.UploadObjectFromFile(ctx, builtPath, cacheBuiltPath)
if err != nil {
return err
}
}
return nil
}
return nil
}
// isBuilt returns whether or not all of a package's modules have been built.
// If the package has an invalid dkms.conf, this returns false.
func isBuilt(pkg *Package) bool {
config, err := LoadConfig(pkg)
if err != nil {
return false
}
for _, module := range config.Modules {
if !fs.IsFile(module.BuiltPath()) {
return false
}
}
return true
}
// IsBuilt returns whether or not a package has been added to the DKMS source
// tree and all of its modules have been built.
func IsBuilt(pkg *Package) bool {
return isBuilt(pkg) && IsAdded(pkg)
}
// IsBuiltInCache returns whether or not all of a package's built modules are
// present in a cache.
// If the package has an invalid dkms.conf, this returns false.
func IsBuiltInCache(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) bool {
config, err := LoadConfig(pkg)
if err != nil {
return false
}
for _, module := range config.Modules {
builtPath := module.CacheBuiltPath()
exists, err := cache.Exists(ctx, builtPath)
if !exists || err != nil {
return false
}
}
return true
}