blob: b0d2d14f3ffb119aa2172ba640c2c98c4b44f371 [file] [log] [blame]
// Copyright 2018 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 publish
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/build"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/pkg"
"go.fuchsia.dev/fuchsia/src/sys/pkg/bin/pm/repo"
far "go.fuchsia.dev/fuchsia/src/sys/pkg/lib/far/go"
)
const (
usage = `Usage: %s publish [-a|-lp] -C -f <file> [-repo <repository directory>]
Pass any one of the mode flags [-a|-lp], and at least one file to pubish.
`
metaFar = "meta.far"
)
type RepeatedArg []string
func (r *RepeatedArg) Set(s string) error {
*r = append(*r, s)
return nil
}
func (r *RepeatedArg) String() string {
return strings.Join(*r, " ")
}
func Run(cfg *build.Config, args []string) error {
fs := flag.NewFlagSet("publish", flag.ExitOnError)
archiveMode := fs.Bool("a", false, "(mode) Publish an archived package.")
listOfPackageManifestsMode := fs.Bool("lp", false, "(mode) Publish a list of packages (and blobs) by package output manifest")
packageSetMode := fs.Bool("ps", false, "(mode) Publish a set of packages from a manifest.")
blobSetMode := fs.Bool("bs", false, "(mode) Publish a set of blobs from a manifest.")
// WARNING: packageset and blobsset modes are deprecated, but are depended upon by infra code.
modeFlags := []*bool{archiveMode, listOfPackageManifestsMode, packageSetMode, blobSetMode}
config := &repo.Config{}
config.Vars(fs)
fs.StringVar(&config.RepoDir, "r", "", "(deprecated, alias for -repo).")
verbose := fs.Bool("v", false, "Print out more informational messages.")
filePaths := RepeatedArg{}
fs.Var(&filePaths, "f", "Path(s) of the file(s) to publish")
clean := fs.Bool("C", false, "\"clean\" the repository. only new publications remain.")
noCreateRepo := fs.Bool("n", false, "If the specified repository path does not exist, do NOT attempt to create it.")
depfilePath := fs.String("depfile", "", "Path to a depfile to write to")
// NOTE(raggi): encryption as implemented is not intended to be a generally used
// feature, as such this flag is deliberately not included in the usage line
encryptionKey := fs.String("e", "", "Path to AES private key for blob encryption")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, usage, filepath.Base(os.Args[0]))
fmt.Fprintln(os.Stderr)
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) != 0 {
fmt.Fprintf(os.Stderr, "WARNING: unused arguments: %s\n", fs.Args())
}
config.ApplyDefaults()
var numModes int
for _, v := range modeFlags {
if *v {
numModes++
}
}
if numModes != 1 {
return fmt.Errorf("exactly one mode flag must be given")
}
if len(filePaths) == 0 {
return fmt.Errorf("no file path supplied")
}
// deps collects a list of all inputs to the publish process to be written to
// depfilePath if requested.
var deps []string
// Make sure the paths to publish actually exist.
for _, k := range filePaths {
if _, err := os.Stat(k); err != nil {
return fmt.Errorf("%q: %s", k, err)
}
}
// allow mkdir to fail, but check if the path exists afterward.
os.MkdirAll(config.RepoDir, os.ModePerm)
fi, err := os.Stat(config.RepoDir)
if err != nil {
return fmt.Errorf("repository path %q is not valid: %s", config.RepoDir, err)
}
if !fi.IsDir() {
return fmt.Errorf("repository path %q is not a directory", config.RepoDir)
}
repo, err := repo.New(config.RepoDir, filepath.Join(config.RepoDir, "repository", "blobs"))
if err != nil {
return fmt.Errorf("error initializing repo: %s", err)
}
if err := repo.OptionallyInitAtLocation(!(*noCreateRepo)); err != nil {
if !os.IsExist(err) {
return err
}
} else {
if err := repo.AddTargets([]string{}, json.RawMessage{}); err != nil {
return err
}
if err := repo.CommitUpdates(config.TimeVersioned); err != nil {
return err
}
if *verbose {
fmt.Printf("initialized repo %s\n", config.RepoDir)
}
}
if *clean {
// Remove any staged items from the repository that are yet to be published.
if err := repo.Clean(); err != nil {
return err
}
// Removes all existing published targets from the repository.
if err := repo.RemoveTargets([]string{}); err != nil {
return err
}
}
if *encryptionKey != "" {
if err := repo.EncryptWith(*encryptionKey); err != nil {
return err
}
deps = append(deps, *encryptionKey)
}
switch {
case *listOfPackageManifestsMode:
debug.SetGCPercent(500)
if len(filePaths) != 1 {
return fmt.Errorf("too many file paths supplied")
}
deps = append(deps, filePaths[0])
f, err := os.Open(filePaths[0])
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
var pkgManifestPaths []string
for scanner.Scan() {
pkgManifestPath := scanner.Text()
if *verbose {
fmt.Printf("publishing: %s\n", pkgManifestPath)
}
pkgManifestPaths = append(pkgManifestPaths, pkgManifestPath)
}
if err := scanner.Err(); err != nil {
return err
}
pkgdeps, err := repo.PublishManifests(pkgManifestPaths)
if err != nil {
return err
}
deps = append(deps, pkgdeps...)
if *verbose {
fmt.Printf("committing updates\n")
}
if err := repo.CommitUpdates(config.TimeVersioned); err != nil {
log.Fatalf("error committing repository updates: %s", err)
}
case *archiveMode:
if len(filePaths) != 1 {
return fmt.Errorf("too many file paths supplied")
}
deps = append(deps, filePaths[0])
f, err := os.Open(filePaths[0])
if err != nil {
return fmt.Errorf("open %s: %s", filePaths[0], err)
}
defer f.Close()
ar, err := far.NewReader(f)
if err != nil {
return fmt.Errorf("open far %s: %s", f.Name(), err)
}
b, err := ar.ReadFile(metaFar)
if err != nil {
return fmt.Errorf("open %s from %s: %s", metaFar, f.Name(), err)
}
mf, err := far.NewReader(bytes.NewReader(b))
if err != nil {
return err
}
pb, err := mf.ReadFile("meta/package")
if err != nil {
return fmt.Errorf("open meta/package from %s from %s: %s", metaFar, f.Name(), err)
}
var p pkg.Package
if err := json.Unmarshal(pb, &p); err != nil {
return err
}
name := p.Name + "/" + p.Version
if *verbose {
fmt.Printf("adding package %s\n", name)
}
if err := repo.AddPackage(name, bytes.NewReader(b), ""); err != nil {
return err
}
for _, n := range ar.List() {
if len(n) != 64 {
continue
}
b, err := ar.ReadFile(n)
if err != nil {
return err
}
if _, _, err := repo.AddBlob(n, bytes.NewReader(b)); err != nil {
return err
}
}
if err := repo.CommitUpdates(config.TimeVersioned); err != nil {
log.Fatalf("error committing repository updates: %s", err)
}
// WARNING: the following two modes are load bearing in infra, but are
// deprecated. They do not have any test coverage. Tread carefully.
case *packageSetMode:
if len(filePaths) != 1 {
return fmt.Errorf("too many file paths supplied")
}
deps = append(deps, filePaths[0])
if err := eachEntry(filePaths[0], func(name, src string) error {
deps = append(deps, src)
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if err := repo.AddPackage(name, f, ""); err != nil {
return fmt.Errorf("failed to add package %q from %q: %s", name, src, err)
}
return nil
}); err != nil {
return err
}
if err := repo.CommitUpdates(config.TimeVersioned); err != nil {
log.Fatalf("error committing repository updates: %s", err)
}
case *blobSetMode:
if len(filePaths) != 1 {
return fmt.Errorf("too many file paths supplied")
}
deps = append(deps, filePaths[0])
if err := eachEntry(filePaths[0], func(merkle, src string) error {
deps = append(deps, src)
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
if _, _, err := repo.AddBlob(merkle, f); err != nil {
return fmt.Errorf("failed to add blob %q from %q: %s", merkle, src, err)
}
return nil
}); err != nil {
return err
}
default:
panic("unhandled mode")
}
if *depfilePath != "" {
timestampPath := filepath.Join(config.RepoDir, "repository", "timestamp.json")
for i, str := range deps {
// Avoid absolute paths in depfiles.
if filepath.IsAbs(str) {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting current working directory: %s", err)
}
rel, err := filepath.Rel(wd, str)
if err != nil {
return fmt.Errorf("rebasing %q to current working directory %q: %s", str, rel, err)
}
str = rel
}
// It is not clear if this is appropriate input for the depfile, which is
// underspecified - it's a "make format file". For the most part this should
// not affect Fuchsia builds, as we do not use "interesting" characters in
// our file names, but if ever we did, this could cause issues. It is at
// least better to try to quote the strings as it is more likely we may see
// filenames or paths containing whitespace than likely less consistent cases
// such as handling invalid or mixed encoding issues.
deps[i] = strconv.Quote(str)
}
depString := strings.Join(deps, " ")
if err := ioutil.WriteFile(*depfilePath, []byte(fmt.Sprintf("%s: %s\n", timestampPath, depString)), 0644); err != nil {
return err
}
}
return nil
}
func eachEntry(path string, cb func(dest, src string) error) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("error reading manifest: %s", err)
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
if err := s.Err(); err != nil {
return fmt.Errorf("error reading manifest: %s", err)
}
line := s.Text()
parts := strings.SplitN(line, "=", 2)
if err := cb(parts[0], parts[1]); err != nil {
return err
}
}
return nil
}