blob: f36d5321337dc0c97f7afad0c04b8345fd22362c [file] [log] [blame]
// 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
}