blob: 122183ada3f6504d146877e6d393e4c2e7267d41 [file] [log] [blame]
// Copyright 2023 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 main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/bundle_fetcher/bundler"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
"github.com/google/subcommands"
)
// A portion of the JSON file, the pieces needed below.
type productBundleMetadataV2 struct {
Version string `json:"version"`
ProductName string `json:"product_name"`
}
type productListCmd struct {
gcsBucket string
buildIDs string
outDir string
outputProductListFileName string
}
const (
buildsDirName = "builds"
buildApiDirName = "build_api"
productBundlesJSONName = "product_bundles.json"
transferJSONName = "transfer.json"
)
func (*productListCmd) Name() string { return "product-list" }
func (*productListCmd) Synopsis() string {
return "Create a product list to index urls to transfer manifest files."
}
func (*productListCmd) Usage() string {
return "bundle_fetcher product-list -bucket <GCS_BUCKET> -build_ids <build_ids>\n"
}
func (cmd *productListCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&cmd.gcsBucket, "bucket", "", "GCS bucket from which to read the files from.")
f.StringVar(&cmd.buildIDs, "build_ids", "", "Comma separated list of build_ids.")
f.StringVar(&cmd.outDir, "out_dir", "", "Directory to write out_file_name to.")
f.StringVar(&cmd.outputProductListFileName, "out_file_name", "product_bundles.json", "Name of the output file containing the product bundle to transfer lookup information.")
}
func (cmd *productListCmd) parseFlags() error {
if cmd.buildIDs == "" {
return fmt.Errorf("-build_ids is required")
}
if cmd.gcsBucket == "" {
return fmt.Errorf("-bucket is required")
}
if cmd.outDir == "" {
return fmt.Errorf("-out_dir is required")
}
info, err := os.Stat(cmd.outDir)
if os.IsNotExist(err) {
return fmt.Errorf("out directory path %s does not exist", cmd.outDir)
}
if err != nil {
return err
}
if !info.IsDir() {
return fmt.Errorf("out directory path %s is not a directory", cmd.outDir)
}
return nil
}
func (cmd *productListCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if err := cmd.execute(ctx); err != nil {
logger.Errorf(ctx, "%s", err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}
func (cmd *productListCmd) execute(ctx context.Context) error {
if err := cmd.parseFlags(); err != nil {
return err
}
sink, err := bundler.NewCloudSink(ctx, cmd.gcsBucket)
if err != nil {
return err
}
defer sink.Close()
return cmd.executeWithSink(ctx, sink)
}
func (cmd *productListCmd) executeWithSink(ctx context.Context, sink bundler.DataSink) error {
productList := build.ProductBundlesManifest{}
buildIDsList := strings.Split(cmd.buildIDs, ",")
for _, buildID := range buildIDsList {
buildID = strings.TrimSpace(buildID)
productBundlePath := filepath.Join(buildsDirName, buildID, buildApiDirName, productBundlesJSONName)
logger.Debugf(ctx, "Build %s contains the product bundles in path %s", buildID, productBundlePath)
productBundles, err := getProductListFromJSON(ctx, sink, productBundlePath)
if err != nil {
return fmt.Errorf("unable to read product bundle metdadata for build_id %s: %s %w", buildID, productBundlePath, err)
}
for _, productBundle := range *productBundles {
productEntry := build.ProductBundle{
// Entries for Label and TransferManifestPath are not needed.
Name: productBundle.Name,
ProductVersion: productBundle.ProductVersion,
TransferManifestUrl: fmt.Sprintf("gs://%s/%s/%s/%s", cmd.gcsBucket, buildsDirName, buildID, transferJSONName),
}
productList = append(productList, productEntry)
}
}
outputFilePath := filepath.Join(cmd.outDir, cmd.outputProductListFileName)
logger.Debugf(ctx, "writing final product list file to: %s", outputFilePath)
f, err := os.Create(outputFilePath)
if err != nil {
return fmt.Errorf("Creating '%s' %w", outputFilePath, err)
}
var errs error
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(&productList); err != nil {
errs = errors.Join(errs, err)
}
if err := f.Close(); err != nil {
errs = errors.Join(errs, err)
}
return errs
}
// Readh the product_bundles.json from GCS and returns them as a
// ProductBundlesManifest list.
func getProductListFromJSON(ctx context.Context, sink bundler.DataSink, productListJSONPath string) (*build.ProductBundlesManifest, error) {
data, err := sink.ReadFromGCS(ctx, productListJSONPath)
if err != nil {
return nil, err
}
listData := &build.ProductBundlesManifest{}
err = json.Unmarshal(data, listData)
if err != nil {
return nil, err
}
if len(*listData) == 0 {
return nil, fmt.Errorf("error, the product list is empty")
}
for _, product := range *listData {
// Only needed values (by this tool) are validated.
if product.Name == "" {
return nil, fmt.Errorf("error, the product name is empty")
}
if product.ProductVersion == "" {
return nil, fmt.Errorf("error, the product version is empty")
}
}
return listData, nil
}