blob: 0c5be4f749e677960c720d9d5457a723139e0a6a [file]
package kernel
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path"
"strings"
"cos.googlesource.com/cos/tools.git/src/pkg/cos"
"cos.googlesource.com/cos/tools.git/src/pkg/git"
"github.com/golang/glog"
)
const (
AppliedPatchesFile = "cos-kernel-applied-patches.txt"
PatchBaseFile = "cos-kernel-patch-base.txt"
)
// Prepare sets up the local kernel directory for compilation.
//
// This will first download the compiler toolchain corresponding to the
// provided kernel variant config, apply any patches from the variant config
// which are not yet applied, and prepare the .config file using the base
// kernel config and fragments from the variant config.
func Prepare(ctx context.Context, downloader cos.ArtifactsDownloader, cleanFailedPatches bool, conf KernelVariantConfig) error {
if err := InstallCompilerToolchainAtomic(ctx, downloader, conf.Toolchain); err != nil {
return fmt.Errorf("could not download compiler toolchain: %v", err)
}
if len(conf.Patches) > 0 {
patchRoots := []string{".", "google/patches"}
patchPaths, err := resolvePatches(conf.Patches, patchRoots)
if err != nil {
return fmt.Errorf("could not resolve all patches: %v", err)
}
if err := applyUnappliedPatches(patchPaths, AppliedPatchesFile, cleanFailedPatches); err != nil {
return fmt.Errorf("could not apply the provided patches: %v", err)
}
}
if len(conf.Configs) == 0 {
return fmt.Errorf("must provide at least one config to prepare")
}
if err := Kmake(conf, conf.Configs); err != nil {
return fmt.Errorf("failed to prepare config %s: %v", conf.Configs[0], err)
}
return nil
}
// resolvePatches gets the paths to all patches matching a provided list of paths.
//
// Each provided path may be the path to a single patch or a directory containing patches.
//
// This will check for paths relative to each provided root directory in order, returning
// the patches from the first path that is found.
func resolvePatches(paths, roots []string) ([]string, error) {
var patches []string
for _, p := range paths {
found := false
for _, root := range roots {
rootRelativePath := path.Join(root, p)
pathPatches, err := resolvePathPatches(rootRelativePath)
if err == nil {
patches = append(patches, pathPatches...)
found = true
break
}
if !errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("error statting patch path %s: %v", p, err)
}
}
if !found {
return nil, fmt.Errorf("could not find patch path %s relative to any root: %v", p, roots)
}
}
return patches, nil
}
// resolvePathPatches gets the paths to all patches matching a path.
//
// The path can either be the path to a single patch or the name of a
// directory containing patches.
func resolvePathPatches(p string) ([]string, error) {
info, err := os.Stat(p)
if err != nil {
return nil, err
}
var patches []string
if info.IsDir() {
entries, err := os.ReadDir(p)
if err != nil {
return nil, fmt.Errorf("could not read patch dir %s: %v", p, err)
}
for _, entry := range entries {
patches = append(patches, path.Join(p, entry.Name()))
}
} else {
patches = append(patches, p)
}
return patches, nil
}
// applyUnappliedPatches applies all the patches from which have not been applied yet from a list of paths.
//
// Each path can be the path to a single patch or a directory containing patches.
//
// If the repo in the current directory isn't clean, this will exit immediatly.
//
// Before applying patches, this will check for an appliedPatchesFile and read its contents. Any
// patch listed in that file will not be reapplied.
// When this function is done writing patches, it will append the list of patches it applied to
// the appliedPatchesFile, creating it if it doesn't already exist.
func applyUnappliedPatches(patches []string, appliedPatchesFile string, cleanFailedPatches bool) error {
clean, err := git.IsClean()
if err != nil {
return fmt.Errorf("refusing to apply patches; failed to check if git repo was clean: %v", err)
}
if !clean {
return fmt.Errorf("git repo is unclean; refusing to apply patches; please stash or reset any changed files")
}
contents, err := os.ReadFile(appliedPatchesFile)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("failed to read %s: %v", appliedPatchesFile, err)
}
alreadyAppliedPatches := strings.Split(strings.Trim(string(contents), "\n"), "\n")
// Get all of the patches from the patch list which are not yet applied.
unappliedPatches := orderedSetDifference(patches, alreadyAppliedPatches)
if unappliedPatches == nil {
return nil
}
glog.Infof(
"[ applying new patches ] %s\n",
strings.Join(unappliedPatches, "\n"),
)
newAppliedPatches, modifiedFiles, err := ApplyPatches(unappliedPatches, os.Stderr)
if len(newAppliedPatches) > 0 {
glog.Infof("applied the following patches: %v", newAppliedPatches)
}
if err != nil || len(newAppliedPatches) != len(unappliedPatches) {
glog.Errorf("could not apply all patches")
var reversedPatches []string
for i := range len(newAppliedPatches) {
reversedPatches = append(reversedPatches, newAppliedPatches[len(newAppliedPatches)-i-1])
}
if cleanFailedPatches {
glog.Errorf("attempting to restore directory to a clean state by unapplying patches (run with --clean-failed-patches=false to avoid this behavior)")
if err := UnapplyPatches(reversedPatches, os.Stderr); err != nil {
return fmt.Errorf("failed to apply patches and failed to restore previous directory state; the directory will need to be cleaned manually: %w", err)
}
glog.Infof("restored directory to clean state")
} else {
var patchUnapplyStrings []string
for _, patch := range reversedPatches {
patchUnapplyStrings = append(patchUnapplyStrings, fmt.Sprintf("patch -p1 --reverse < %q", patch))
}
unapplyCommand := strings.Join(patchUnapplyStrings, " && ")
fmt.Fprintf(os.Stderr, "\nThe following command can be run to unapply the newly applied patches and restore the directory to a clean state:\n%s\n\n", unapplyCommand)
}
return fmt.Errorf("failed to apply unapplied patches: %v", err)
}
patchesLines := strings.Join(patches, "\n")
allPatches := append(alreadyAppliedPatches, unappliedPatches...)
allPatchesLines := []byte(strings.Join(allPatches, "\n"))
if err := os.WriteFile(appliedPatchesFile, allPatchesLines, 0666); err != nil {
return fmt.Errorf("failed to write new patches lines: %v", err)
}
modifiedFiles = append(modifiedFiles, appliedPatchesFile)
if err := git.Add(modifiedFiles, os.Stderr); err != nil {
return fmt.Errorf("failed to `git add` %s: %v", appliedPatchesFile, err)
}
commitMessage := fmt.Sprintf(
"prepare: Applied patches\n\nApplied the following patches:\n%s\n",
patchesLines,
)
if err := git.Commit(commitMessage, os.Stderr); err != nil {
return fmt.Errorf("failed to commit patch info: %v", err)
}
return nil
}
// orderedSetDifference returns all of the items in set A which are not in
// set B, while preserving the order of the items in A.
func orderedSetDifference(a, b []string) []string {
setB := make(map[string]bool)
for _, item := range b {
setB[item] = true
}
var difference []string
for _, item := range a {
_, found := setB[item]
if !found {
difference = append(difference, item)
}
}
return difference
}
// PrepareBranch switches to a new branch and sets up the local kernel
// directory for compilation.
//
// See `Prepare` for details on how the branch will be prepared.
//
// The new branch will be marked with a `cos-kernel-branch-start.txt` file,
// which contains the name of the branch. This can be used as a reference
// when creating patches from the new branch.
func PrepareBranch(ctx context.Context, downloader cos.ArtifactsDownloader, cleanFailedPatches bool, conf KernelVariantConfig, branch string) error {
if err := git.SwitchToNewBranch(branch, os.Stderr); err != nil {
return fmt.Errorf("could not switch to branch %s to prepare it: %v", branch, err)
}
if err := Prepare(ctx, downloader, cleanFailedPatches, conf); err != nil {
return fmt.Errorf("failed to prepare branch %s: %v", branch, err)
}
if err := createPatchBase(PatchBaseFile, branch); err != nil {
return fmt.Errorf("failed to create patch base: %v", err)
}
return nil
}
// createPatchBase creates the patch base file for a feature and commits it to the repo.
func createPatchBase(patchBaseFile, feature string) error {
if err := os.WriteFile(patchBaseFile, []byte(feature), 0664); err != nil {
return fmt.Errorf("failed to write %s to mark patch base for feature %s: %v", patchBaseFile, feature, err)
}
if err := git.Add([]string{patchBaseFile}, os.Stderr); err != nil {
return fmt.Errorf("failed to add patch base file %s: %v", patchBaseFile, err)
}
commitMessage := fmt.Sprintf("prepare: patch base %s", feature)
if err := git.Commit(commitMessage, os.Stderr); err != nil {
return fmt.Errorf("failed to commit patch base: %v", err)
}
return nil
}
// ReadPatchBase reads the patch base file created by PrepareBranch.
func ReadPatchBase() (string, error) {
contents, err := os.ReadFile(PatchBaseFile)
if err != nil {
return "", fmt.Errorf("failed to get patch base: %v", err)
}
return strings.TrimSpace(string(contents)), nil
}
// FindPatchBaseCommit finds the hash of the base commit for a branch
// created by PrepareBranch(..., branch).
func FindPatchBaseCommit(branch string) (string, error) {
pattern := fmt.Sprintf("^prepare: patch base %s$", branch)
commit, err := git.FindCommitByPattern(pattern)
if err != nil {
return "", fmt.Errorf("error searching for pattern %s: %v", pattern, err)
}
return commit, nil
}