| // Copyright 2020 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 packages |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| |
| versionHistory "go.fuchsia.dev/fuchsia/src/lib/versioning/version-history/go" |
| "go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build" |
| "go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg" |
| "go.fuchsia.dev/fuchsia/tools/lib/logger" |
| ) |
| |
| type PackageBuilder struct { |
| Name string |
| Repository string |
| Version string |
| Cache string |
| Contents map[string]string |
| } |
| |
| func parsePackageJSON(path string) (string, string, error) { |
| jsonData, err := os.ReadFile(path) |
| if err != nil { |
| return "", "", fmt.Errorf("failed to read file at %s: %w", path, err) |
| } |
| var packageInfo pkg.Package |
| if err := json.Unmarshal(jsonData, &packageInfo); err != nil { |
| return "", "", fmt.Errorf("failed to unmarshal json data: %w", err) |
| } |
| return packageInfo.Name, packageInfo.Version, nil |
| } |
| |
| // NewPackageBuilder returns a PackageBuilder |
| // Must call `Close()` to clean up PackageBuilder |
| func NewPackageBuilder(name string, version string, repository string) (*PackageBuilder, error) { |
| if name == "" || version == "" { |
| return nil, fmt.Errorf("missing package info and version information") |
| } |
| |
| // Create temporary directory to store any additions that come in. |
| tempDir, err := os.MkdirTemp("", "pm-temp-resource") |
| if err != nil { |
| return nil, fmt.Errorf("failed to create temp directory: %w", err) |
| } |
| |
| return &PackageBuilder{ |
| Name: name, |
| Repository: repository, |
| Version: version, |
| Cache: tempDir, |
| Contents: make(map[string]string), |
| }, nil |
| } |
| |
| // NewPackageBuilderFromDir returns a PackageBuilder that initializes from the `dir` package directory. |
| // Must call `Close()` to clean up PackageBuilder |
| func NewPackageBuilderFromDir(dir string, name string, version string, repository string) (*PackageBuilder, error) { |
| pkg, err := NewPackageBuilder(name, version, repository) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return fmt.Errorf("walk of %s failed: %w", dir, err) |
| } |
| if !info.IsDir() { |
| relativePath := strings.Replace(path, dir+"/", "", 1) |
| pkg.Contents[relativePath] = path |
| } |
| return nil |
| }) |
| if err != nil { |
| return nil, fmt.Errorf("error when walking the directory: %w", err) |
| } |
| |
| return pkg, nil |
| } |
| |
| // Close removes temporary directories created by PackageBuilder. |
| func (p *PackageBuilder) Close() { |
| os.RemoveAll(p.Cache) |
| } |
| |
| // AddResource adds a resource to the package at the given path. |
| func (p *PackageBuilder) AddResource(path string, contents io.Reader) error { |
| if _, ok := p.Contents[path]; ok { |
| return fmt.Errorf("a resource already exists at path %q", path) |
| } |
| data, err := io.ReadAll(contents) |
| if err != nil { |
| return fmt.Errorf("failed to read file: %w", err) |
| } |
| tempPath := filepath.Join(p.Cache, path) |
| if err := os.MkdirAll(filepath.Dir(tempPath), os.ModePerm); err != nil { |
| return fmt.Errorf("failed to create parent directories for %q: %w", tempPath, err) |
| } |
| if err = os.WriteFile(tempPath, data, 0644); err != nil { |
| return fmt.Errorf("failed to write data to %q: %w", tempPath, err) |
| } |
| p.Contents[path] = tempPath |
| return nil |
| } |
| |
| func tempConfig(dir string, name string, version string, repository string) (*build.Config, error) { |
| |
| cfg := &build.Config{ |
| OutputDir: filepath.Join(dir, "output"), |
| ManifestPath: filepath.Join(dir, "manifest"), |
| KeyPath: filepath.Join(dir, "key"), |
| TempDir: filepath.Join(dir, "tmp"), |
| PkgName: name, |
| PkgVersion: version, |
| PkgRepository: repository, |
| PkgABIRevision: versionHistory.History().ExampleSupportedAbiRevisionForTests(), |
| } |
| |
| for _, d := range []string{cfg.OutputDir, cfg.TempDir} { |
| if err := os.MkdirAll(d, os.ModePerm); err != nil { |
| return nil, err |
| } |
| } |
| |
| return cfg, nil |
| } |
| |
| // Publish the package to the repository. Returns the TUF package path and |
| // merkle on success, or a error on failure. |
| func (p *PackageBuilder) Publish(ctx context.Context, pkgRepo *Repository) (Package, error) { |
| // Create Config. |
| dir, err := os.MkdirTemp("", "pm-temp-config") |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create temp directory for the config: %w", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| cfg, err := tempConfig(dir, p.Name, p.Version, p.Repository) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create temp config to fill with our data: %w", err) |
| } |
| |
| pkgManifestPath := filepath.Join(filepath.Dir(cfg.ManifestPath), "package") |
| if err := os.MkdirAll(filepath.Join(pkgManifestPath, "meta"), os.ModePerm); err != nil { |
| return Package{}, fmt.Errorf("failed to make parent dirs for meta/package: %w", err) |
| } |
| mfst, err := os.Create(cfg.ManifestPath) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create package manifest path: %w", err) |
| } |
| defer mfst.Close() |
| |
| // Fill config with our contents. |
| for relativePath, sourcePath := range p.Contents { |
| if relativePath == "meta/contents" { |
| continue |
| } |
| if _, err := fmt.Fprintf(mfst, "%s=%s\n", relativePath, sourcePath); err != nil { |
| return Package{}, fmt.Errorf("failed to record entry %q as %q into manifest: %w", p.Name, sourcePath, err) |
| } |
| } |
| |
| manifest, err := cfg.Manifest() |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to create mainfest: %w", err) |
| } |
| |
| abiPath, ok := manifest.Meta()["meta/fuchsia.abi/abi-revision"] |
| if ok { |
| abiRevision, err := build.ReadABIRevisionFromFile(abiPath) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to get abi revision: %w", err) |
| } |
| cfg.PkgABIRevision = *abiRevision |
| } |
| |
| // Save changes to config. |
| if err := build.Update(cfg); err != nil { |
| return Package{}, fmt.Errorf("failed to update config: %w", err) |
| } |
| if _, err := build.Seal(cfg); err != nil { |
| return Package{}, fmt.Errorf("failed to seal config: %w", err) |
| } |
| |
| pkgPath := fmt.Sprintf("%s/%s", p.Name, p.Version) |
| blobs, err := cfg.BlobInfo() |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to extract blobs: %w", err) |
| } |
| |
| foundPkgMerkle := false |
| pkgMerkle := build.MerkleRoot{} |
| for _, blob := range blobs { |
| if blob.Path == "meta/" { |
| foundPkgMerkle = true |
| pkgMerkle = blob.Merkle |
| break |
| } |
| } |
| |
| if !foundPkgMerkle { |
| return Package{}, fmt.Errorf("could not find meta.far merkle") |
| } |
| |
| logger.Infof(ctx, "publishing %q to merkle %q", pkgPath, pkgMerkle) |
| |
| outputManifest, err := cfg.OutputManifest() |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to output manifest: %w", err) |
| } |
| |
| content, err := json.Marshal(outputManifest) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to convert manifest to JSON: %w", err) |
| } |
| |
| outputManifestPath := path.Join(cfg.OutputDir, "package_manifest.json") |
| if err := os.WriteFile(outputManifestPath, content, os.ModePerm); err != nil { |
| return Package{}, fmt.Errorf("failed to write manifest JSON to %q: %w", outputManifestPath, err) |
| } |
| |
| // Publish new config to repo. |
| err = pkgRepo.Publish(ctx, outputManifestPath) |
| if err != nil { |
| return Package{}, fmt.Errorf("failed to publish manifest: %w", err) |
| } |
| logger.Infof(ctx, "package %q as %q published and committed", pkgPath, pkgMerkle) |
| |
| return newPackage(ctx, pkgRepo, pkgPath, pkgMerkle) |
| } |