package kernelcat import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "sort" ) // DiscoveredArtifacts is what DiscoverPaths returns: absolute paths to a // kernel, an optional initrd, and an optional modules directory located // under the staged output of make-*-kernel.sh (or an equivalent layout). type DiscoveredArtifacts struct { KernelPath string InitrdPath string ModulesDir string } // metadataFile is the optional JSON a kernel-build script can drop // alongside its staged output to point ReadLocal at specific filenames // without guessing. type metadataFile struct { KernelPath string `json:"kernel_path"` InitrdPath string `json:"initrd_path"` ModulesDir string `json:"modules_dir"` } // DiscoverPaths locates kernel / initrd / modules artifacts under fromDir. // It prefers a metadata.json emitted by make-*-kernel.sh; otherwise it // falls back to globbing boot/vmlinux-*, boot/vmlinuz-* (for Alpine), // boot/initramfs-*, and the newest subdir under lib/modules/. func DiscoverPaths(fromDir string) (DiscoveredArtifacts, error) { info, err := os.Stat(fromDir) if err != nil { return DiscoveredArtifacts{}, err } if !info.IsDir() { return DiscoveredArtifacts{}, fmt.Errorf("%s is not a directory", fromDir) } if paths, ok, err := discoverFromMetadata(fromDir); err != nil { return DiscoveredArtifacts{}, err } else if ok { return paths, nil } bootDir := filepath.Join(fromDir, "boot") kernel, err := latestMatch(bootDir, []string{"vmlinux-*", "vmlinuz-*"}) if err != nil { return DiscoveredArtifacts{}, fmt.Errorf("locate kernel under %s: %w", bootDir, err) } initrd, err := latestMatch(bootDir, []string{"initramfs-*"}) if err != nil && !errors.Is(err, os.ErrNotExist) { return DiscoveredArtifacts{}, fmt.Errorf("locate initrd under %s: %w", bootDir, err) } modules, err := latestSubdir(filepath.Join(fromDir, "lib", "modules")) if err != nil && !errors.Is(err, os.ErrNotExist) { return DiscoveredArtifacts{}, fmt.Errorf("locate modules under %s: %w", fromDir, err) } return DiscoveredArtifacts{ KernelPath: kernel, InitrdPath: initrd, ModulesDir: modules, }, nil } func discoverFromMetadata(fromDir string) (DiscoveredArtifacts, bool, error) { data, err := os.ReadFile(filepath.Join(fromDir, "metadata.json")) if err != nil { if errors.Is(err, os.ErrNotExist) { return DiscoveredArtifacts{}, false, nil } return DiscoveredArtifacts{}, false, err } var meta metadataFile if err := json.Unmarshal(data, &meta); err != nil { return DiscoveredArtifacts{}, false, fmt.Errorf("parse metadata.json in %s: %w", fromDir, err) } kernel := absoluteOrAnchored(fromDir, meta.KernelPath) if kernel == "" { return DiscoveredArtifacts{}, false, nil } if _, err := os.Stat(kernel); err != nil { return DiscoveredArtifacts{}, false, fmt.Errorf("metadata.json references missing kernel %s: %w", kernel, err) } out := DiscoveredArtifacts{KernelPath: kernel} if meta.InitrdPath != "" { candidate := absoluteOrAnchored(fromDir, meta.InitrdPath) if _, err := os.Stat(candidate); err == nil { out.InitrdPath = candidate } } if meta.ModulesDir != "" { candidate := absoluteOrAnchored(fromDir, meta.ModulesDir) if info, err := os.Stat(candidate); err == nil && info.IsDir() { out.ModulesDir = candidate } } return out, true, nil } // absoluteOrAnchored returns path as-is if absolute; otherwise joins it to // anchor. Empty input returns "". func absoluteOrAnchored(anchor, path string) string { path = filepath.Clean(path) if path == "" || path == "." { return "" } if filepath.IsAbs(path) { return path } return filepath.Join(anchor, path) } // latestMatch returns the lexicographically latest file in dir matching any // of patterns (filename globs, not full paths). Returns os.ErrNotExist if no // match. func latestMatch(dir string, patterns []string) (string, error) { if _, err := os.Stat(dir); err != nil { return "", err } entries, err := os.ReadDir(dir) if err != nil { return "", err } var matches []string for _, entry := range entries { if entry.IsDir() { continue } for _, pattern := range patterns { ok, _ := filepath.Match(pattern, entry.Name()) if ok { matches = append(matches, entry.Name()) break } } } if len(matches) == 0 { return "", os.ErrNotExist } sort.Strings(matches) return filepath.Join(dir, matches[len(matches)-1]), nil } // latestSubdir returns the lexicographically latest subdirectory of root. // Returns os.ErrNotExist if root is missing or has no subdirs. func latestSubdir(root string) (string, error) { if _, err := os.Stat(root); err != nil { return "", err } entries, err := os.ReadDir(root) if err != nil { return "", err } var dirs []string for _, entry := range entries { if entry.IsDir() { dirs = append(dirs, entry.Name()) } } if len(dirs) == 0 { return "", os.ErrNotExist } sort.Strings(dirs) return filepath.Join(root, dirs[len(dirs)-1]), nil }