| 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 |
| } |