package bundle import ( "fmt" "os" "path" "path/filepath" "sort" "time" ) func BuildManifest(opts BuildOptions) (Manifest, error) { if opts.Root == "" { return Manifest{}, fmt.Errorf("root is required") } if opts.ID == "" { return Manifest{}, fmt.Errorf("id is required") } explicit := len(opts.Files) > 0 if explicit == opts.Scan { return Manifest{}, fmt.Errorf("select exactly one file mode") } created := opts.Created if created.IsZero() { created = time.Now().UTC() } paths := append([]string(nil), opts.Files...) var err error if opts.Scan { paths, err = scanSourcePaths(opts.Root) if err != nil { return Manifest{}, err } } files := make([]ManifestFile, 0, len(paths)) for _, sourcePath := range paths { file, err := buildManifestFile(opts.Root, sourcePath) if err != nil { return Manifest{}, err } files = append(files, file) } manifest := Manifest{ SchemaVersion: SchemaVersion, ID: opts.ID, Created: created, Files: files, } manifest.Digest = BundleDigest(manifest.Files) if err := ValidateManifest(manifest); err != nil { return Manifest{}, err } return manifest, nil } func scanSourcePaths(root string) ([]string, error) { var paths []string err := filepath.WalkDir(root, func(filePath string, entry os.DirEntry, err error) error { if err != nil { return err } if filePath == root { return nil } relative, err := filepath.Rel(root, filePath) if err != nil { return err } sourcePath := filepath.ToSlash(relative) if entry.Type()&os.ModeSymlink != 0 { return fmt.Errorf("source file %q must be a regular file", sourcePath) } if entry.IsDir() { return nil } if path.Base(sourcePath) == ManifestName || path.Base(sourcePath) == distributorStateName { return nil } info, err := entry.Info() if err != nil { return err } if !info.Mode().IsRegular() { return fmt.Errorf("source file %q must be a regular file", sourcePath) } paths = append(paths, sourcePath) return nil }) if err != nil { return nil, err } sort.Strings(paths) return paths, nil } func buildManifestFile(root, sourcePath string) (ManifestFile, error) { if err := ValidateSourcePath(sourcePath); err != nil { return ManifestFile{}, fmt.Errorf("source file %q: %w", sourcePath, err) } fullPath := filepath.Join(root, filepath.FromSlash(sourcePath)) info, err := os.Lstat(fullPath) if err != nil { return ManifestFile{}, fmt.Errorf("source file %q stat: %w", sourcePath, err) } if !info.Mode().IsRegular() { return ManifestFile{}, fmt.Errorf("source file %q must be a regular file", sourcePath) } data, err := os.ReadFile(fullPath) if err != nil { return ManifestFile{}, fmt.Errorf("source file %q read: %w", sourcePath, err) } return ManifestFile{ Path: sourcePath, SHA256: FileDigest(data), Size: int64(len(data)), }, nil }