| // 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 |
| } |