blob: c202c079949d117f3ce31b3c0b7e54cb6f06068e [file] [log] [blame]
// Copyright 2019 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 artifacts
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"golang.org/x/crypto/ssh"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/avb"
pmBuild "go.fuchsia.dev/fuchsia/src/testing/host-target-testing/build"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/omaha_tool"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/packages"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/paver"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/zbi"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
type BlobFetchMode int
const (
// PrefetchBlobs will download all the blobs from a build when `GetPackageRepository()` is called.
PrefetchBlobs BlobFetchMode = iota
// LazilyFetchBlobs will only download blobs when they are accessed.
LazilyFetchBlobs
// Product Bundle manifest which is used to locate VBmeta
ProductBundleManifest = "product_bundle.json"
relativeBootserverPath = "tools/linux-x64/bootserver"
relativeFfxPath = "tools/linux-x64/ffx"
relativeFlashManifest = "images/flash.json"
productBundleBuildInfoPath = "build_api/build_info.json"
)
type Build interface {
// String returns a string identifier for this build.
String() string
// OutputDir is a location where we can write generated test files.
OutputDir() string
// GetBootserver returns the path to the bootserver used for paving.
GetBootserver(ctx context.Context) (string, error)
// GetFfx returns the FFXTool from this build.
GetFfx(
ctx context.Context,
ffxIsolateDir ffx.IsolateDir,
) (*ffx.FFXTool, error)
// GetFlashManifest returns the path to the flash manifest used for flashing.
GetFlashManifest(ctx context.Context) (string, error)
// GetProductBundleDir returns the path to the product bundle.
GetProductBundleDir(ctx context.Context) (string, error)
// GetPackageRepository returns a Repository for this build.
GetPackageRepository(
ctx context.Context,
blobFetchMode BlobFetchMode,
ffxIsolateDir ffx.IsolateDir,
) (*packages.Repository, error)
// GetPaverDir downloads and returns the directory containing the images
// and image manifest.
GetPaverDir(ctx context.Context) (string, error)
// GetPaver downloads and returns a paver for the build.
GetPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (paver.Paver, error)
// GetVbmetaPath downloads and returns a path to the zircon-a vbmeta image.
GetVbmetaPath(ctx context.Context) (string, error)
}
// ArtifactsBuild represents the build artifacts for a specific build.
type ArtifactsBuild struct {
id string
archive *Archive
blobsDir string
buildDir string
packages *packages.Repository
buildImageDir string
productBundleDir string
srcs map[string]struct{}
}
func (b *ArtifactsBuild) OutputDir() string {
return b.buildDir
}
func (b *ArtifactsBuild) GetBootserver(ctx context.Context) (string, error) {
// We need build images to flash, so lets download them.
_, err := b.GetBuildImages(ctx)
if err != nil {
return "", err
}
// Use the latest bootserver if possible because the one uploaded with the artifacts may not include bug fixes.
currentBuildId := os.Getenv("BUILDBUCKET_ID")
if currentBuildId == "" {
currentBuildId = b.id
}
bootserverPath := filepath.Join(b.buildDir, relativeBootserverPath)
if err := b.archive.download(
ctx,
currentBuildId,
false,
b.buildDir,
[]string{relativeBootserverPath},
); err != nil {
return "", fmt.Errorf("failed to download bootserver: %w", err)
}
// Make bootserver executable.
if err := os.Chmod(bootserverPath, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to make bootserver executable: %w", err)
}
return bootserverPath, nil
}
func (b *ArtifactsBuild) GetFfx(
ctx context.Context,
ffxIsolateDir ffx.IsolateDir,
) (*ffx.FFXTool, error) {
// Use the latest ffx
ffxPath := filepath.Join(b.buildDir, relativeFfxPath)
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
[]string{relativeFfxPath},
); err != nil {
return nil, fmt.Errorf("failed to download ffxPath: %w", err)
}
// Ignore error, not all builds upload ffx-package subtools
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
[]string{relativeFfxPath + "-package"},
); err == nil {
// Make ffx-package executable.
if err := os.Chmod(ffxPath+"-package", os.ModePerm); err != nil {
return nil, fmt.Errorf("failed to make ffx-package executable: %w", err)
}
}
// Make ffx executable.
if err := os.Chmod(ffxPath, os.ModePerm); err != nil {
return nil, fmt.Errorf("failed to make ffxPath executable: %w", err)
}
return ffx.NewFFXTool(ffxPath, ffxIsolateDir)
}
func (b *ArtifactsBuild) GetFlashManifest(ctx context.Context) (string, error) {
// We need build images to flash.
_, err := b.GetBuildImages(ctx)
if err != nil {
return "", err
}
flashManifest := filepath.Join(b.buildDir, relativeFlashManifest)
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
[]string{relativeFlashManifest},
); err != nil {
return "", fmt.Errorf("failed to download flash.json for flasher: %w", err)
}
return flashManifest, nil
}
func (b *ArtifactsBuild) GetProductBundleDir(ctx context.Context) (string, error) {
if b.productBundleDir != "" {
return b.productBundleDir, nil
}
logger.Infof(ctx, "downloading product bundle for build %s", b.id)
buildInfoPath := filepath.Join(b.buildDir, productBundleBuildInfoPath)
// Fetch build_info.json, this is needed to construct the PB path in GCS
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
[]string{productBundleBuildInfoPath},
); err != nil {
logger.Errorf(ctx, "failed to download build info for build %s: %v", b.id, err)
return "", fmt.Errorf("failed to download build info for build %s: %w", b.id, err)
}
f, err := os.Open(buildInfoPath)
if err != nil {
return "", err
}
defer f.Close()
// BuildInfo represent the build info build-api.
var buildInfo struct {
// Conguration is the array of product/board dictionary.
Configurations []struct {
// Board is the board name. e.g. "x64", "vim3".
Board string `json:"board"`
// Product is the product name. e.g. "minimal", "core".
Product string `json:"product"`
} `json:"configurations"`
// A unique version of this build.
Version string `json:"version"`
}
if err := json.NewDecoder(f).Decode(&buildInfo); err != nil {
return "", fmt.Errorf("failed to read build info: %w", err)
}
// The first entry is the product bundle for the build.
config := buildInfo.Configurations[0]
// Fetch PB from GCS
pbPath := fmt.Sprintf("product_bundles/%s.%s", config.Product, config.Board)
artifacts, err := b.archive.list(ctx, b.id)
if err != nil {
return "", err
}
pbArtifacts := []string{}
prefix := pbPath + "/"
for _, artifact := range artifacts {
if strings.HasPrefix(artifact, prefix) {
pbArtifacts = append(pbArtifacts, artifact)
}
}
productBundleDir := filepath.Join(b.buildDir, pbPath)
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
pbArtifacts,
); err != nil {
logger.Errorf(ctx, "failed to download product bundle for build %s: %v", b.id, err)
return "", fmt.Errorf("failed to download product bundle for build %s: %w", b.id, err)
}
b.productBundleDir = productBundleDir
return b.productBundleDir, nil
}
// GetPackageRepository returns a Repository for this build. It tries to
// download a package when all the artifacts are stored in individual files,
// which is how modern builds publish their build artifacts.
func (b *ArtifactsBuild) GetPackageRepository(
ctx context.Context,
fetchMode BlobFetchMode,
ffxIsolateDir ffx.IsolateDir,
) (*packages.Repository, error) {
if b.packages != nil {
return b.packages, nil
}
logger.Infof(ctx, "downloading package repository")
// Make sure the blob contains the `packages/all_blobs.json`.
if _, ok := b.srcs["packages/all_blobs.json"]; !ok {
logger.Errorf(ctx, "blobs manifest doesn't exist for build %s", b.id)
return nil, fmt.Errorf("blob manifest doesn't exist for build %s", b.id)
}
packageSrcs := []string{}
for src := range b.srcs {
if strings.HasPrefix(src, "packages/") {
packageSrcs = append(packageSrcs, src)
}
}
packagesDir := filepath.Join(b.buildDir, "packages")
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
packageSrcs,
); err != nil {
logger.Errorf(ctx, "failed to download packages for build %s to %s: %v", packagesDir, b.id, err)
return nil, fmt.Errorf("failed to download packages for build %s to %s: %w", packagesDir, b.id, err)
}
blobsManifest := filepath.Join(packagesDir, "all_blobs.json")
blobsData, err := os.ReadFile(blobsManifest)
if err != nil {
return nil, fmt.Errorf("failed to read blobs manifest: %w", err)
}
var blobs []build.Blob
err = json.Unmarshal(blobsData, &blobs)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal blobs JSON: %w", err)
}
deliveryBlobConfigPath := filepath.Join(packagesDir, "delivery_blob_config.json")
relativeBlobsDir, err := build.GetBlobsDir(deliveryBlobConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to get blobs dir: %w", err)
}
var blobsList []string
for _, blob := range blobs {
blobsList = append(blobsList, filepath.Join(relativeBlobsDir, blob.Merkle))
}
logger.Infof(ctx, "all_blobs contains %d blobs", len(blobs))
if fetchMode == PrefetchBlobs {
if err := b.archive.download(
ctx,
b.id,
true,
filepath.Join(b.blobsDir),
blobsList,
); err != nil {
logger.Errorf(ctx, "failed to download blobs to %s: %v", b.blobsDir, err)
return nil, fmt.Errorf("failed to download blobs to %s: %w", b.blobsDir, err)
}
}
ffx, err := b.GetFfx(ctx, ffxIsolateDir)
if err != nil {
return nil, fmt.Errorf("failed to get ffx: %w", err)
}
blobType, err := build.GetDeliveryBlobType(deliveryBlobConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to get delivery blob type: %w", err)
}
p, err := packages.NewRepository(
ctx,
packagesDir,
&proxyBlobStore{
b: b,
ffx: ffx,
blobsDir: b.blobsDir,
blobType: blobType,
},
ffx,
blobType,
)
if err != nil {
return nil, err
}
b.packages = p
return b.packages, nil
}
type proxyBlobStore struct {
b *ArtifactsBuild
ffx *ffx.FFXTool
blobsDir string
blobType *int
}
func (fs *proxyBlobStore) PrefetchBlobs(
ctx context.Context,
deliveryBlobType *int,
merkles []pmBuild.MerkleRoot,
) error {
if len(merkles) == 0 {
return nil
}
var relativeBlobsDir string
if deliveryBlobType == nil {
relativeBlobsDir = "blobs"
} else {
deliveryBlobType := strconv.Itoa(*deliveryBlobType)
relativeBlobsDir = filepath.Join("blobs", deliveryBlobType)
}
srcs := []string{}
for _, merkle := range merkles {
src := filepath.Join(relativeBlobsDir, merkle.String())
srcs = append(srcs, src)
}
// Start downloading the blobs. The package resolver will only fetch a blob
// once, so we don't need to deduplicate requests on our side.
return fs.b.archive.download(
ctx,
fs.b.id,
true,
filepath.Dir(fs.blobsDir),
srcs,
)
}
func (fs *proxyBlobStore) BlobPath(ctx context.Context, deliveryBlobType *int, merkle pmBuild.MerkleRoot) (string, error) {
var path string
if deliveryBlobType == nil {
path = filepath.Join(fs.blobsDir, merkle.String())
} else {
path = filepath.Join(fs.blobsDir, strconv.Itoa(*deliveryBlobType), merkle.String())
}
// First, try to read the blob from the directory
if _, err := os.Stat(path); err == nil {
return path, nil
}
// Decompress delivery blob if possible.
if deliveryBlobType == nil && fs.blobType != nil && fs.ffx.SupportsPackageBlob(ctx) {
deliveryBlobPath := filepath.Join(fs.blobsDir, strconv.Itoa(*fs.blobType), merkle.String())
_, err := os.Stat(deliveryBlobPath)
if err != nil {
err = fs.PrefetchBlobs(ctx, fs.blobType, []pmBuild.MerkleRoot{merkle})
if err != nil {
logger.Errorf(ctx, "failed to prefetch type %d blob %s: %v", *fs.blobType, merkle, err)
}
}
if err == nil {
err = fs.ffx.DecompressBlobs(ctx, []string{deliveryBlobPath}, fs.blobsDir)
if err == nil {
return path, nil
}
logger.Errorf(ctx, "failed to decompress blob %s: %v", deliveryBlobPath, err)
}
}
if err := fs.PrefetchBlobs(ctx, deliveryBlobType, []pmBuild.MerkleRoot{merkle}); err != nil {
return "", err
}
return path, nil
}
func (fs *proxyBlobStore) OpenBlob(ctx context.Context, deliveryBlobType *int, merkle pmBuild.MerkleRoot) (*os.File, error) {
path, err := fs.BlobPath(ctx, deliveryBlobType, merkle)
if err != nil {
return nil, err
}
return os.Open(path)
}
func (fs *proxyBlobStore) BlobSize(ctx context.Context, deliveryBlobType *int, merkle pmBuild.MerkleRoot) (uint64, error) {
f, err := fs.OpenBlob(ctx, deliveryBlobType, merkle)
if err != nil {
return 0, err
}
defer f.Close()
if s, err := f.Stat(); err == nil {
if s.Size() < 0 {
return 0, fmt.Errorf("merkle %s has size less than zero: %d", merkle, s.Size())
}
return uint64(s.Size()), nil
} else {
return 0, err
}
}
func (fs *proxyBlobStore) Dir() string {
return fs.blobsDir
}
// GetBuildImages downloads the build images for a specific build id.
// Returns a path to the directory of the downloaded images or an error if it
// fails to download.
func (b *ArtifactsBuild) GetBuildImages(ctx context.Context) (string, error) {
if b.buildImageDir != "" {
return b.buildImageDir, nil
}
logger.Infof(ctx, "downloading build images")
imageDir := filepath.Join(b.buildDir, "images")
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
[]string{path.Join("images", paver.ImageManifest)},
); err != nil {
return "", fmt.Errorf("failed to download image manifest: %w", err)
}
imagesJSON := filepath.Join(imageDir, paver.ImageManifest)
f, err := os.Open(imagesJSON)
if err != nil {
return "", fmt.Errorf("failed to open %q: %w", imagesJSON, err)
}
defer f.Close()
var items []build.Image
if err := json.NewDecoder(f).Decode(&items); err != nil {
return "", fmt.Errorf("failed to parse build image %q: %w", imagesJSON, err)
}
// Get list of all available images to download and only download
// the ones needed for flashing or paving.
imageSrcMap := make(map[string]struct{})
for src := range b.srcs {
if strings.HasPrefix(src, "images/") {
imageSrcMap[src] = struct{}{}
}
}
imageSrcs := []string{}
for _, item := range items {
src := path.Join("images", item.Path)
if _, ok := imageSrcMap[src]; ok {
imageSrcs = append(imageSrcs, src)
}
}
if err := b.archive.download(
ctx,
b.id,
false,
b.buildDir,
imageSrcs,
); err != nil {
return "", fmt.Errorf("failed to download images to %s: %w", imageDir, err)
}
b.buildImageDir = imageDir
return b.buildImageDir, nil
}
func (b *ArtifactsBuild) GetPaverDir(ctx context.Context) (string, error) {
return b.GetBuildImages(ctx)
}
// GetPaver downloads and returns a paver for the build.
func (b *ArtifactsBuild) GetPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (paver.Paver, error) {
return b.getPaver(ctx, sshPublicKey)
}
func (b *ArtifactsBuild) getPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (*paver.BuildPaver, error) {
buildImageDir, err := b.GetPaverDir(ctx)
if err != nil {
return nil, err
}
bootserverPath, err := b.GetBootserver(ctx)
if err != nil {
return nil, err
}
return paver.NewBuildPaver(bootserverPath, buildImageDir, paver.SSHPublicKey(sshPublicKey))
}
func (b *ArtifactsBuild) GetVbmetaPath(ctx context.Context) (string, error) {
path, err := b.getVbmetaPathFromProductBundle(ctx)
if err == nil {
return path, nil
}
logger.Warningf(ctx, "failed to get vbmeta from product bundle, trying images: %v", err)
path, err = b.getVbmetaPathFromImages(ctx)
if err != nil {
return "", err
}
return path, nil
}
func (b *ArtifactsBuild) getVbmetaPathFromProductBundle(ctx context.Context) (string, error) {
productBundleDir, err := b.GetProductBundleDir(ctx)
if err != nil {
return "", err
}
productBundle, err := util.ParseProductBundle(productBundleDir)
if err != nil {
return "", err
}
vbmetaRelativePath, err := productBundle.GetSystemAImage("vbmeta", "zircon-a")
if err != nil {
return "", err
}
return filepath.Join(productBundleDir, vbmetaRelativePath), nil
}
func (b *ArtifactsBuild) getVbmetaPathFromImages(ctx context.Context) (string, error) {
buildImageDir, err := b.GetBuildImages(ctx)
if err != nil {
return "", err
}
imagesJSON := filepath.Join(buildImageDir, paver.ImageManifest)
f, err := os.Open(imagesJSON)
if err != nil {
return "", fmt.Errorf("failed to open %q: %w", imagesJSON, err)
}
defer f.Close()
var items []struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
if err := json.NewDecoder(f).Decode(&items); err != nil {
return "", fmt.Errorf("failed to parse vbmeta image %q: %w", imagesJSON, err)
}
for _, item := range items {
if item.Name == "zircon-a" && item.Type == "vbmeta" {
return filepath.Join(buildImageDir, item.Path), nil
}
}
return "", fmt.Errorf("failed to file zircon-a vbmeta in %q", imagesJSON)
}
func (b *ArtifactsBuild) String() string {
return b.id
}
type FuchsiaDirBuild struct {
dir string
}
func NewFuchsiaDirBuild(
dir string,
) *FuchsiaDirBuild {
return &FuchsiaDirBuild{
dir: dir,
}
}
func (b *FuchsiaDirBuild) String() string {
return b.dir
}
func (b *FuchsiaDirBuild) OutputDir() string {
return b.dir
}
func (b *FuchsiaDirBuild) GetBootserver(ctx context.Context) (string, error) {
return filepath.Join(b.dir, "host_x64/bootserver_new"), nil
}
func (b *FuchsiaDirBuild) GetFfx(
ctx context.Context,
ffxIsolateDir ffx.IsolateDir,
) (*ffx.FFXTool, error) {
ffxPath := filepath.Join(b.dir, "host_x64/ffx")
return ffx.NewFFXTool(ffxPath, ffxIsolateDir)
}
func (b *FuchsiaDirBuild) GetFlashManifest(ctx context.Context) (string, error) {
return filepath.Join(b.dir, "flash.json"), nil
}
func (b *FuchsiaDirBuild) GetProductBundleDir(ctx context.Context) (string, error) {
productBundlesPath := filepath.Join(b.dir, "product_bundles.json")
f, err := os.Open(productBundlesPath)
if err != nil {
return "", fmt.Errorf("failed to open %s: %w", productBundlesPath, err)
}
defer f.Close()
var productBundles []struct {
Path string `json:"path"`
}
if err := json.NewDecoder(f).Decode(&productBundles); err != nil {
return "", fmt.Errorf("failed to parse product bundles path %s: %w", productBundlesPath, err)
}
// The first entry is the product bundle for the build.
return filepath.Join(b.dir, productBundles[0].Path), nil
}
func (b *FuchsiaDirBuild) GetPackageRepository(
ctx context.Context,
blobFetchMode BlobFetchMode,
ffxIsolateDir ffx.IsolateDir,
) (*packages.Repository, error) {
ffx, err := b.GetFfx(ctx, ffxIsolateDir)
if err != nil {
return nil, fmt.Errorf("failed to get ffx: %w", err)
}
blobFS := packages.NewDirBlobStore(filepath.Join(b.dir, "amber-files", "repository", "blobs"))
blobType, err := build.GetDeliveryBlobType(filepath.Join(b.dir, "delivery_blob_config.json"))
if err != nil {
return nil, fmt.Errorf("failed to get delivery blob type: %w", err)
}
return packages.NewRepository(ctx, filepath.Join(b.dir, "amber-files"), blobFS, ffx, blobType)
}
func (b *FuchsiaDirBuild) GetPaverDir(ctx context.Context) (string, error) {
return b.dir, nil
}
func (b *FuchsiaDirBuild) GetPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (paver.Paver, error) {
bootserverPath, err := b.GetBootserver(ctx)
if err != nil {
return nil, err
}
return paver.NewBuildPaver(bootserverPath, b.dir, paver.SSHPublicKey(sshPublicKey))
}
func (b *FuchsiaDirBuild) GetVbmetaPath(ctx context.Context) (string, error) {
imagesJSON := filepath.Join(b.dir, paver.ImageManifest)
f, err := os.Open(imagesJSON)
if err != nil {
return "", fmt.Errorf("failed to open %q: %w", imagesJSON, err)
}
defer f.Close()
var items []build.Image
if err := json.NewDecoder(f).Decode(&items); err != nil {
return "", fmt.Errorf("failed to parse vbmeta imagesJSON %q: %w", imagesJSON, err)
}
for _, item := range items {
if item.Name == "zircon-a" && item.Type == "vbmeta" {
return filepath.Join(b.dir, item.Path), nil
}
}
return "", fmt.Errorf("failed to file zircon-a vbmeta in %q", imagesJSON)
}
type ProductBundleDirBuild struct {
productBundleDir string
outputDir string
}
func NewProductBundleDirBuild(
productBundleDir string,
) *ProductBundleDirBuild {
return &ProductBundleDirBuild{
productBundleDir: productBundleDir,
}
}
func (b *ProductBundleDirBuild) String() string {
return b.productBundleDir
}
func (b *ProductBundleDirBuild) OutputDir() string {
return b.outputDir
}
func (b *ProductBundleDirBuild) GetBootserver(ctx context.Context) (string, error) {
// Only flashing is supported for ProductBundle
return "", fmt.Errorf("paving is not supported with product bundles")
}
func (b *ProductBundleDirBuild) GetFfx(
ctx context.Context,
ffxIsolateDir ffx.IsolateDir,
) (*ffx.FFXTool, error) {
ffxPath := filepath.Join(b.productBundleDir, "ffx")
return ffx.NewFFXTool(ffxPath, ffxIsolateDir)
}
func (b *ProductBundleDirBuild) GetFlashManifest(ctx context.Context) (string, error) {
return b.productBundleDir, nil
}
func (b *ProductBundleDirBuild) GetProductBundleDir(ctx context.Context) (string, error) {
return b.productBundleDir, nil
}
func (b *ProductBundleDirBuild) GetPackageRepository(
ctx context.Context,
blobFetchMode BlobFetchMode,
ffxIsolateDir ffx.IsolateDir,
) (*packages.Repository, error) {
ffx, err := b.GetFfx(ctx, ffxIsolateDir)
if err != nil {
return nil, fmt.Errorf("failed to get ffx: %w", err)
}
productBundle, err := util.ParseProductBundle(b.productBundleDir)
if err != nil {
return nil, err
}
blobFS := packages.NewDirBlobStore(
filepath.Join(b.productBundleDir, productBundle.Repositories[0].BlobsPath),
)
// TODO(https://fxbug.dev/42076853): Read delivery blob type from product bundle.
return packages.NewRepository(ctx, b.productBundleDir, blobFS, ffx, nil)
}
func (b *ProductBundleDirBuild) GetPaverDir(ctx context.Context) (string, error) {
// Only flashing is supported for product bundles
return "", fmt.Errorf("paving is not supported with product bundles")
}
func (b *ProductBundleDirBuild) GetPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (paver.Paver, error) {
// Only flashing is supported for product bundles
return nil, fmt.Errorf("paving is not supported with product bundles")
}
func (b *ProductBundleDirBuild) GetVbmetaPath(ctx context.Context) (string, error) {
productBundle, err := util.ParseProductBundle(b.productBundleDir)
if err != nil {
return "", err
}
relativeVbmetaPath, err := productBundle.GetSystemAImage("vbmeta", "zircon-a")
if err != nil {
return "", err
}
return filepath.Join(b.productBundleDir, relativeVbmetaPath), nil
}
type OmahaBuild struct {
build Build
omahatool *omaha_tool.OmahaTool
avbtool *avb.AVBTool
zbitool *zbi.ZBITool
}
func NewOmahaBuild(
build Build,
omahatool *omaha_tool.OmahaTool,
avbtool *avb.AVBTool,
zbitool *zbi.ZBITool,
) *OmahaBuild {
return &OmahaBuild{
build: build,
omahatool: omahatool,
avbtool: avbtool,
zbitool: zbitool,
}
}
func (b *OmahaBuild) String() string {
return b.build.String()
}
func (b *OmahaBuild) OutputDir() string {
return b.build.OutputDir()
}
func (b *OmahaBuild) GetBootserver(ctx context.Context) (string, error) {
return b.build.GetBootserver(ctx)
}
func (b *OmahaBuild) GetFfx(
ctx context.Context,
ffxIsolateDir ffx.IsolateDir,
) (*ffx.FFXTool, error) {
return b.build.GetFfx(ctx, ffxIsolateDir)
}
type versionedFlashManifest struct {
Version int `json:"version"`
Manifest flashManifest `json:"manifest"`
}
type flashManifest struct {
Credentials []string `json:"credentials,omitempty"`
HwRevision string `json:"hw_revision"`
Products []flashProduct `json:"products,omitempty"`
}
type flashProduct struct {
BootloaderPartitions []flashPartition `json:"bootloader_partitions,omitempty"`
Name string `json:"name"`
Partitions []flashPartition `json:"partitions,omitempty"`
RequiresUnlock bool `json:"requires_unlock"`
}
type flashPartition struct {
Name string `json:"name"`
Path string `json:"path"`
Condition *flashCondition `json:"condition,omitempty"`
}
type flashCondition struct {
Value string `json:"value"`
Variable string `json:"variable"`
}
func (b *OmahaBuild) GetFlashManifest(ctx context.Context) (string, error) {
flashManifestPath, err := b.build.GetFlashManifest(ctx)
if err != nil {
return "", err
}
fileInfo, err := os.Stat(flashManifestPath)
if err != nil {
return "", err
}
// Error out if we're dealing with a product bundle build.
if fileInfo.IsDir() {
return "", fmt.Errorf(
"flashing product bundles with omaha builds currently not supported",
)
}
f, err := os.Open(flashManifestPath)
if err != nil {
return "", err
}
defer f.Close()
var manifest versionedFlashManifest
decoder := json.NewDecoder(f)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return "", err
}
if manifest.Version != 3 {
return "", fmt.Errorf("Unknown flash manifest version %d", manifest.Version)
}
srcVbmetaPath, err := b.GetVbmetaPath(ctx)
if err != nil {
return "", fmt.Errorf("failed to find zircon-a vbmeta: %w", err)
}
paverDir, err := b.GetPaverDir(ctx)
if err != nil {
return "", err
}
destVbmetaPath := filepath.Join(paverDir, "zircon-a-omaha-test.vbmeta")
if err := b.updateVBMeta(ctx, srcVbmetaPath, destVbmetaPath); err != nil {
return "", err
}
// Update the manifest to point at the new vbmeta.
for i, product := range manifest.Manifest.Products {
if product.Name == "fuchsia" || product.Name == "fuchsia_only" {
for j, partition := range product.Partitions {
if partition.Name == "vbmeta_a" || partition.Name == "vbmeta_b" {
manifest.Manifest.Products[i].Partitions[j].Path = destVbmetaPath
}
}
}
}
// Write out the manifest to a new file and return it..
updatedFlashManifest := filepath.Join(paverDir, "flash-omaha-test.json")
f, err = os.Create(updatedFlashManifest)
if err != nil {
return "", err
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
if err := encoder.Encode(&manifest); err != nil {
return "", err
}
return updatedFlashManifest, nil
}
func (b *OmahaBuild) GetProductBundleDir(ctx context.Context) (string, error) {
productBundleDir, err := b.build.GetProductBundleDir(ctx)
if err != nil {
return "", fmt.Errorf("failed to get product bundle dir for %s: %w", b, err)
}
// We need to copy the product bundle so we can overwrite the vbmeta to
// inject the omaha url into the vbmeta.
newProductBundleDir := filepath.Join(b.OutputDir(), "product_bundle_ota_test")
if err := util.CopyDir(ctx, newProductBundleDir, productBundleDir); err != nil {
return "", err
}
productBundle, err := util.ParseProductBundle(newProductBundleDir)
if err != nil {
return "", err
}
relativeSrcVbmetaPath, err := productBundle.GetSystemAImage("vbmeta", "zircon-a")
if err != nil {
return "", err
}
srcVbmetaPath := filepath.Join(productBundleDir, relativeSrcVbmetaPath)
relativeDestVbmetaPath := "zircon-a-omaha-test.vbmeta"
destVbmetaPath := filepath.Join(newProductBundleDir, relativeDestVbmetaPath)
if err := b.updateVBMeta(ctx, srcVbmetaPath, destVbmetaPath); err != nil {
return "", err
}
for i, image := range productBundle.SystemA {
if image.Type == "vbmeta" && image.Name == "zircon-a" {
productBundle.SystemA[i].Path = relativeDestVbmetaPath
}
}
if err := util.UpdateProductBundle(newProductBundleDir, productBundle); err != nil {
return "", err
}
return newProductBundleDir, nil
}
func (b *OmahaBuild) updateVBMeta(
ctx context.Context,
srcVbmetaPath string,
destVbmetaPath string,
) error {
// Create a ZBI with the omaha_url argument.
tempDir, err := os.MkdirTemp("", "")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Create a ZBI with the omaha_url argument.
destZbiPath := path.Join(tempDir, "omaha_argument.zbi")
imageArguments := map[string]string{
"omaha_url": b.omahatool.URL(),
"omaha_app_id": b.omahatool.Args.AppId,
"ota_channel": "ota-test-channel",
}
if err := b.zbitool.MakeImageArgsZbi(ctx, destZbiPath, imageArguments); err != nil {
return fmt.Errorf("Failed to create ZBI: %w", err)
}
// Create a vbmeta that includes the ZBI we just created.
propFiles := map[string]string{
"zbi": destZbiPath,
}
err = b.avbtool.MakeVBMetaImage(ctx, destVbmetaPath, srcVbmetaPath, propFiles)
if err != nil {
return fmt.Errorf("failed to create vbmeta: %w", err)
}
return nil
}
// GetPackageRepository returns a Repository for this build.
func (b *OmahaBuild) GetPackageRepository(
ctx context.Context,
blobFetchMode BlobFetchMode,
ffxIsolateDir ffx.IsolateDir,
) (*packages.Repository, error) {
return b.build.GetPackageRepository(ctx, blobFetchMode, ffxIsolateDir)
}
func (b *OmahaBuild) GetPaverDir(ctx context.Context) (string, error) {
return b.build.GetPaverDir(ctx)
}
// GetPaver downloads and returns a paver for the build.
func (b *OmahaBuild) GetPaver(
ctx context.Context,
sshPublicKey ssh.PublicKey,
) (paver.Paver, error) {
paverDir, err := b.GetPaverDir(ctx)
if err != nil {
return nil, err
}
bootserverPath, err := b.GetBootserver(ctx)
if err != nil {
return nil, err
}
srcVbmetaPath, err := b.GetVbmetaPath(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find zircon-a vbmeta: %w", err)
}
destVbmetaPath := filepath.Join(paverDir, "zircon-a-omaha-test.vbmeta")
if err := b.updateVBMeta(ctx, srcVbmetaPath, destVbmetaPath); err != nil {
return nil, err
}
return paver.NewBuildPaver(
bootserverPath,
paverDir,
paver.SSHPublicKey(sshPublicKey),
paver.OverrideVBMetaA(destVbmetaPath),
)
}
func (b *OmahaBuild) GetVbmetaPath(ctx context.Context) (string, error) {
return b.build.GetVbmetaPath(ctx)
}