| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package build |
| |
| import ( |
| "bufio" |
| "encoding/binary" |
| "encoding/json" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "sync" |
| |
| "go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg" |
| far "go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go" |
| "go.fuchsia.dev/fuchsia/src/sys/pkg/lib/merkle" |
| ) |
| |
| const abiRevisionKey string = "meta/fuchsia.abi/abi-revision" |
| |
| // invalidRepositoryCharsPattern contains all characters not allowed by the spec in |
| // https://fuchsia.dev/fuchsia-src/concepts/packages/package_url#repository |
| var InvalidRepositoryCharsPattern = regexp.MustCompile("[^a-z0-9-.]").MatchString |
| |
| // Build-time information about a subpackage |
| type SubpackageInfo struct { |
| // If provided, the subpackage name (relative URI path), relative to the |
| // parent package. |
| Name *string `json:"name,omitempty"` |
| |
| // If a subpackage-specific Name override is not explicitly provided, |
| // this field is the path to a JSON serialized pkg.Package object |
| // containing the subpackage's declared package_name. |
| MetaPackageFile *string `json:"meta_package_file,omitempty"` |
| |
| // The path to the package_manifest.json of the subpackage (may not be |
| // used by pm tool) |
| PackageManifestFile string `json:"package_manifest_file"` |
| } |
| |
| // subpackages metafile content |
| type MetaSubpackages struct { |
| Version string `json:"version"` |
| Subpackages map[string]string `json:"subpackages"` |
| } |
| |
| // PackageManifest is the json structure representation of a full package |
| // manifest. |
| type PackageManifest struct { |
| Version string `json:"version"` |
| Repository string `json:"repository,omitempty"` |
| Package pkg.Package `json:"package"` |
| Blobs []PackageBlobInfo `json:"blobs"` |
| Subpackages []PackageSubpackageInfo `json:"subpackages,omitempty"` |
| } |
| |
| // packageManifestMaybeRelative is the json structure representation of a package |
| // manifest that may contain file-relative source paths. This is a separate type |
| // from PackageManifest so we don't need to touch every use of PackageManifest to |
| // avoid writing invalid blob_sources_relative values to disk. |
| type packageManifestMaybeRelative struct { |
| Version string `json:"version"` |
| Repository string `json:"repository,omitempty"` |
| Package pkg.Package `json:"package"` |
| Blobs []PackageBlobInfo `json:"blobs"` |
| Subpackages []PackageSubpackageInfo `json:"subpackages,omitempty"` |
| // TODO(https://fxbug.dev/42066050): rename the json field to `paths_relative` since it |
| // applies to both blobs and subpackages. |
| PathsRelativeTo string `json:"blob_sources_relative"` |
| } |
| |
| // LoadPackageManifest parses the package manifest for a particular package, |
| // resolving file-relative blob source paths before returning if needed. |
| func LoadPackageManifest(packageManifestPath string) (*PackageManifest, error) { |
| fileContents, err := os.ReadFile(packageManifestPath) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read %s: %w", packageManifestPath, err) |
| } |
| |
| rawManifest := &packageManifestMaybeRelative{} |
| if err := json.Unmarshal(fileContents, rawManifest); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal %s: %w", packageManifestPath, err) |
| } |
| |
| manifest := &PackageManifest{} |
| manifest.Version = rawManifest.Version |
| manifest.Repository = rawManifest.Repository |
| manifest.Package = rawManifest.Package |
| |
| if manifest.Version != "1" { |
| return nil, fmt.Errorf("unknown version %q, can't load manifest", manifest.Version) |
| } |
| |
| // if the manifest has file-relative paths, make them relative to the working directory |
| if rawManifest.PathsRelativeTo == "file" { |
| basePath := filepath.Dir(packageManifestPath) |
| for i := 0; i < len(rawManifest.Blobs); i++ { |
| blob := rawManifest.Blobs[i] |
| blob.SourcePath = filepath.Join(basePath, blob.SourcePath) |
| manifest.Blobs = append(manifest.Blobs, blob) |
| } |
| |
| for i := 0; i < len(rawManifest.Subpackages); i++ { |
| subpackage := rawManifest.Subpackages[i] |
| subpackage.ManifestPath = filepath.Join(basePath, subpackage.ManifestPath) |
| manifest.Subpackages = append(manifest.Subpackages, subpackage) |
| } |
| } else { |
| manifest.Blobs = rawManifest.Blobs |
| manifest.Subpackages = rawManifest.Subpackages |
| } |
| |
| return manifest, nil |
| } |
| |
| // Init initializes package metadata in the output directory. A manifest |
| // is generated with a name matching the output directory name. |
| func Init(cfg *Config) error { |
| metadir := filepath.Join(cfg.OutputDir, "meta") |
| if err := os.MkdirAll(metadir, os.ModePerm); err != nil { |
| return err |
| } |
| |
| meta := filepath.Join(metadir, "package") |
| if _, err := os.Stat(meta); os.IsNotExist(err) { |
| f, err := os.Create(meta) |
| if err != nil { |
| return err |
| } |
| |
| p, err := cfg.Package() |
| if err != nil { |
| return err |
| } |
| |
| err = json.NewEncoder(f).Encode(&p) |
| f.Close() |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Update walks the contents of the package and updates the merkle root values |
| // within the contents file. |
| func Update(cfg *Config) error { |
| metadir := filepath.Join(cfg.OutputDir, "meta") |
| os.MkdirAll(metadir, os.ModePerm) |
| manifest, err := cfg.Manifest() |
| if err != nil { |
| return err |
| } |
| |
| if err := writeABIRevision(cfg, manifest); err != nil { |
| return err |
| } |
| |
| contentsPath := filepath.Join(metadir, "contents") |
| pkgContents := manifest.Content() |
| |
| if cfg.SubpackagesPath != "" { |
| if err := writeSubpackagesMeta(cfg, cfg.SubpackagesPath); err != nil { |
| return err |
| } |
| } |
| |
| // manifestLines is a channel containing unpacked manifest paths |
| var manifestLines = make(chan struct{ src, dest string }, len(pkgContents)) |
| go func() { |
| for dest, src := range pkgContents { |
| manifestLines <- struct{ src, dest string }{src, dest} |
| } |
| close(manifestLines) |
| }() |
| |
| // contentCollector receives entries to include in contents |
| type contentEntry struct { |
| path string |
| root MerkleRoot |
| } |
| var contentCollector = make(chan contentEntry, len(pkgContents)) |
| var errors = make(chan error) |
| |
| // w is a group that is done when contentCollector is fully populated |
| var w sync.WaitGroup |
| for i := runtime.NumCPU(); i > 0; i-- { |
| w.Add(1) |
| |
| go func() { |
| defer w.Done() |
| |
| for in := range manifestLines { |
| var t merkle.Tree |
| cf, err := os.Open(in.src) |
| if err != nil { |
| errors <- fmt.Errorf("build.Update: open %s for %s: %s", in.src, in.dest, err) |
| return |
| } |
| _, err = t.ReadFrom(bufio.NewReader(cf)) |
| cf.Close() |
| if err != nil { |
| errors <- err |
| return |
| } |
| |
| var root MerkleRoot |
| copy(root[:], t.Root()) |
| contentCollector <- contentEntry{in.dest, root} |
| } |
| }() |
| } |
| |
| // close the collector channel when all workers are done |
| go func() { |
| w.Wait() |
| close(contentCollector) |
| }() |
| |
| // collect all results and close done to signal the waiting select |
| var done = make(chan struct{}) |
| contents := MetaContents{} |
| go func() { |
| for entry := range contentCollector { |
| contents[entry.path] = entry.root |
| } |
| close(done) |
| }() |
| |
| select { |
| case <-done: |
| // contents is populated |
| case err := <-errors: |
| // exit on the first error |
| return err |
| } |
| |
| manifest.Paths["meta/contents"] = contentsPath |
| |
| return os.WriteFile(contentsPath, |
| []byte(contents.String()), os.ModePerm) |
| } |
| |
| func writeABIRevision(cfg *Config, manifest *Manifest) error { |
| // Read the ABI file from the manifest, if it exists. |
| manifestABIRevision, err := readABIRevision(manifest) |
| if err != nil { |
| return err |
| } |
| |
| if manifestABIRevision == nil && cfg.PkgABIRevision == 0 { |
| // FIXME(https://fxbug.dev/42168415): We can stop treating the ABI |
| // revision as optional once the ecosystem has migrated to |
| // specifying it everywhere. |
| //return fmt.Errorf("the Fuchsia SDK version must be specified with either the -api-level or -abi-revision arguments, or manually creating meta/fuchsia.abi/abi-revision in the package directory") |
| return nil |
| } |
| |
| var abiRevision uint64 |
| |
| if manifestABIRevision != nil { |
| if cfg.PkgABIRevision != 0 && *manifestABIRevision != cfg.PkgABIRevision { |
| return fmt.Errorf("Manifest ABI revision %x does not match the CLI revision %x", *manifestABIRevision, cfg.PkgABIRevision) |
| } |
| |
| abiRevision = *manifestABIRevision |
| } else { |
| abiRevision = cfg.PkgABIRevision |
| } |
| |
| abiDir := filepath.Join(cfg.OutputDir, "meta", "fuchsia.abi") |
| if err := os.MkdirAll(abiDir, os.ModePerm); err != nil { |
| return err |
| } |
| |
| b := make([]byte, 8) |
| binary.LittleEndian.PutUint64(b, abiRevision) |
| |
| path := filepath.Join(abiDir, "abi-revision") |
| if err := os.WriteFile(path, b, os.ModePerm); err != nil { |
| return err |
| } |
| |
| manifest.Paths[abiRevisionKey] = path |
| |
| return nil |
| } |
| |
| func readABIRevision(manifest *Manifest) (*uint64, error) { |
| abiPath, ok := manifest.Meta()[abiRevisionKey] |
| if !ok { |
| return nil, nil |
| } |
| |
| return ReadABIRevisionFromFile(abiPath) |
| } |
| |
| func ReadABIRevisionFromFile(abiPath string) (*uint64, error) { |
| f, err := os.Open(abiPath) |
| if err != nil { |
| return nil, err |
| } |
| |
| // The ABI revision file should be exactly 8 bytes. |
| info, err := f.Stat() |
| if err != nil { |
| return nil, err |
| } |
| |
| if info.Size() != 8 { |
| return nil, fmt.Errorf("ABI revision file must be 8 bytes") |
| } |
| |
| abiRevisionBytes, err := io.ReadAll(f) |
| if err != nil { |
| return nil, err |
| } |
| |
| // We should have only read 8 bytes. |
| if len(abiRevisionBytes) != 8 { |
| return nil, fmt.Errorf("Invalid ABI revision") |
| } |
| |
| abiRevision := binary.LittleEndian.Uint64(abiRevisionBytes) |
| |
| return &abiRevision, nil |
| } |
| |
| // ErrRequiredFileMissing is returned by operations when the operation depends |
| // on a file that was not found on disk. |
| type ErrRequiredFileMissing struct { |
| Path string |
| } |
| |
| func (e ErrRequiredFileMissing) Error() string { |
| return fmt.Sprintf("pkg: missing required file: %q", e.Path) |
| } |
| |
| // RequiredFiles is a list of files that are required before a package can be sealed. |
| var RequiredFiles = []string{"meta/contents", "meta/package"} |
| |
| // Validate ensures that the package contains the required files. |
| func Validate(cfg *Config) error { |
| if InvalidRepositoryCharsPattern(cfg.PkgRepository) { |
| return fmt.Errorf("pkg: invalid package repository \"%v\"", cfg.PkgRepository) |
| } |
| manifest, err := cfg.Manifest() |
| if err != nil { |
| return err |
| } |
| meta := manifest.Meta() |
| |
| for _, f := range RequiredFiles { |
| if _, ok := meta[f]; !ok { |
| return ErrRequiredFileMissing{f} |
| } |
| } |
| |
| return nil |
| } |
| |
| // Seal archives meta/ into a FAR archive named meta.far. |
| func Seal(cfg *Config) (string, error) { |
| manifest, err := cfg.Manifest() |
| if err != nil { |
| return "", err |
| } |
| |
| if err := Validate(cfg); err != nil { |
| return "", err |
| } |
| |
| archive, err := os.Create(cfg.MetaFAR()) |
| if err != nil { |
| return "", err |
| } |
| |
| if err := far.Write(archive, manifest.Meta()); err != nil { |
| return "", err |
| } |
| |
| if _, err := archive.Seek(0, io.SeekStart); err != nil { |
| return "", err |
| } |
| return cfg.MetaFAR(), archive.Close() |
| } |
| |
| // Read the build-time subpackage data and output files and generate the |
| // "subpackages" meta file |
| func writeSubpackagesMeta(cfg *Config, subpackagesPath string) error { |
| content, err := os.ReadFile(subpackagesPath) |
| if err != nil { |
| return fmt.Errorf("the -subpackages file (%s) could not be read: %s", subpackagesPath, err) |
| } |
| var subpackages []SubpackageInfo |
| if err := json.Unmarshal(content, &subpackages); err != nil { |
| return fmt.Errorf("the -subpackages file (%s) could not be parsed as JSON: %s", subpackagesPath, err) |
| } |
| |
| var meta_subpackages MetaSubpackages |
| meta_subpackages.Version = "1" |
| meta_subpackages.Subpackages = make(map[string]string) |
| |
| for _, subpackage := range subpackages { |
| var name *string |
| if subpackage.Name != nil { |
| name = subpackage.Name |
| } else if subpackage.MetaPackageFile == nil { |
| return fmt.Errorf("format error in the -subpackages file (%s): a subpackage entry requires either a 'name' or a 'meta_package_file'", subpackagesPath) |
| } else { |
| content, err := os.ReadFile(*subpackage.MetaPackageFile) |
| if err != nil { |
| return fmt.Errorf("a subpackage meta package file (%s) could not be read: %s", *subpackage.MetaPackageFile, err) |
| } |
| var packageMeta pkg.Package |
| if err := json.Unmarshal(content, &packageMeta); err != nil { |
| return fmt.Errorf("a subpackage meta package file (%s) could not be parsed as JSON: %s", *subpackage.MetaPackageFile, err) |
| } |
| name = &packageMeta.Name |
| } |
| |
| if _, ok := meta_subpackages.Subpackages[*name]; ok { |
| return fmt.Errorf("duplicate entry in the -subpackages file (%s) for name: %s", subpackagesPath, *name) |
| } |
| |
| // Find the merkle root for the subpackage. |
| packageManifest, err := LoadPackageManifest(subpackage.PackageManifestFile) |
| if err != nil { |
| return fmt.Errorf("a subpackage package manifest (%s) could not be read: %s", subpackage.PackageManifestFile, err) |
| } |
| metaFarIdx := -1 |
| for i := range packageManifest.Blobs { |
| if packageManifest.Blobs[i].Path == "meta/" { |
| metaFarIdx = i |
| break |
| } |
| } |
| if metaFarIdx == -1 { |
| return fmt.Errorf("a subpackage package manifest (%s) is missing a meta.far: %s", subpackage.PackageManifestFile, err) |
| } |
| merkle := packageManifest.Blobs[metaFarIdx].Merkle.String() |
| meta_subpackages.Subpackages[*name] = merkle |
| } |
| { |
| content, err := json.MarshalIndent(meta_subpackages, "", " ") |
| if err != nil { |
| return err |
| } |
| subpackages_dir := filepath.Join(cfg.OutputDir, "meta", "fuchsia.pkg") |
| os.MkdirAll(subpackages_dir, os.ModePerm) |
| subpackagesFilePath := filepath.Join(subpackages_dir, "subpackages") |
| if err := os.WriteFile(subpackagesFilePath, content, 0644); err != nil { |
| return err |
| } |
| |
| manifest, err := cfg.Manifest() |
| if err != nil { |
| return err |
| } |
| manifest.Paths["meta/fuchsia.pkg/subpackages"] = subpackagesFilePath |
| } |
| return nil |
| } |