blob: 921513cfa8c7d596f77f62f735e0cf69d4a1cd3e [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"text/template"
"cros.local/bazel/ebuild/private/common/bazelutil"
"cros.local/bazel/ebuild/private/common/portage/binarypackage"
"cros.local/bazel/ebuild/private/common/portage/dependency"
"cros.local/bazel/ebuild/private/common/tar"
)
var (
repoNameToPath = map[string]string{
"cros": "third_party/chromiumos-overlay",
"portage-stable": "third_party/portage-stable",
}
)
type Needed struct {
arch string
absLibPath string
soname string
rpaths []string
needed []string
multilib string
}
type packageMetadata struct {
overlayPath string
atom *dependency.Atom
needed []Needed
files []tar.FileListItem
sharedLibs []string
// The list of .so's required by the shared libs defined in `sharedLibs`.
sharedLibDeps []string
// The list of .so's required by the ELF binaries
binLibDeps []string
}
var sharedLibRegex = regexp.MustCompile(
`^(?P<linkername>\w+\.so)(:?\.(?P<major>\d+)(:?\.(?P<minor>\d+)(:?\.(?P<rev>\d+))?)?)?$`)
// Ignore libs provided by glibc
func ignoredSharedLib(name string) bool {
if name == "" {
return true
} else if strings.HasPrefix(name, "ld-linux-") {
return true
} else if strings.HasPrefix(name, "libc.so.") {
return true
} else if strings.HasPrefix(name, "libpthread.so.") {
return true
}
return false
}
func newPackageMetadata(overlayName string, atom *dependency.Atom, needed []Needed, files []tar.FileListItem) (*packageMetadata, error) {
for _, file := range files {
if file.Path == "" {
return nil, fmt.Errorf("File path is empty!")
}
if !strings.HasPrefix(file.Path, "./") {
return nil, fmt.Errorf("File path is not relative: %s", file.Path)
}
}
var sharedLibs []string
sharedLibDeps := make(map[string]struct{})
binLibDeps := make(map[string]struct{})
providedSoNames := make(map[string]struct{})
// Verify all packages produce a fully qualified version number.
for _, lib := range needed {
baseName := filepath.Base(lib.absLibPath)
matches := sharedLibRegex.FindStringSubmatch(baseName)
if matches == nil { // ELF binary
for _, dep := range lib.needed {
if !ignoredSharedLib(dep) {
binLibDeps[dep] = struct{}{}
}
}
continue
} else { // .so library
if len(matches) != 8 {
return nil, fmt.Errorf("%s must have the following format: libX.so.X.X.X", lib.absLibPath)
}
var soName string
if matches[3] != "" {
soName = fmt.Sprintf("%s.%s", matches[1], matches[3])
} else {
// Some .so's aren't versioned
soName = matches[1]
}
providedSoNames[soName] = struct{}{}
sharedLibs = append(sharedLibs, lib.absLibPath)
for _, dep := range lib.needed {
if !ignoredSharedLib(dep) {
sharedLibDeps[dep] = struct{}{}
}
}
}
}
sharedLibDepsList := make([]string, 0, len(sharedLibDeps))
for dep := range sharedLibDeps {
if _, ok := providedSoNames[dep]; ok {
// Don't list provided so's as deps
continue
}
sharedLibDepsList = append(sharedLibDepsList, dep)
}
sort.Strings(sharedLibDepsList)
binLibDepsList := make([]string, 0, len(binLibDeps))
for dep := range binLibDeps {
if _, ok := providedSoNames[dep]; ok {
// Don't list provided so's as deps
continue
}
if _, ok := sharedLibDeps[dep]; ok {
// dep is already listed
continue
}
binLibDepsList = append(binLibDepsList, dep)
}
sort.Strings(binLibDepsList)
overlayPath, ok := repoNameToPath[overlayName]
if !ok {
return nil, fmt.Errorf("Unknown mapping for overlay: %s", overlayName)
}
return &packageMetadata{overlayPath, atom, needed, files, sharedLibs, sharedLibDepsList, binLibDepsList}, nil
}
func (m packageMetadata) IsEmpty() bool {
return (len(m.Headers()) == 0 &&
len(m.PkgConfig()) == 0 &&
len(m.StaticLibs()) == 0 &&
len(m.SharedLibs()) == 0)
}
func (m packageMetadata) EbuildPath() string {
name := strings.Split(m.atom.PackageName(), "/")[1]
return fmt.Sprintf("%s/%s/%s-%s.ebuild", m.overlayPath, m.atom.PackageName(), name, m.atom.Version())
}
func (m packageMetadata) BinPkgFileName() string {
name := strings.Split(m.atom.PackageName(), "/")[1]
return fmt.Sprintf("%s-%s.tbz2", name, m.atom.Version())
}
func (m packageMetadata) Headers() []string {
var items []string
for _, file := range m.files {
if !strings.Contains(file.Path, "/include/") {
continue
}
items = append(items, file.Path[1:])
}
return items
}
func (m packageMetadata) PkgConfig() []string {
var items []string
for _, file := range m.files {
if !strings.HasSuffix(file.Path, ".pc") {
continue
}
items = append(items, file.Path[1:])
}
return items
}
func (m packageMetadata) StaticLibs() []string {
var items []string
for _, file := range m.files {
if !strings.HasSuffix(file.Path, ".a") {
continue
}
items = append(items, file.Path[1:])
}
return items
}
func (m packageMetadata) SharedLibs() []string {
return m.sharedLibs
}
func (m packageMetadata) SharedLibDeps() []string {
return m.sharedLibDeps
}
func (m packageMetadata) BinLibDeps() []string {
return m.binLibDeps
}
func parseNeeded(xpak binarypackage.XPAK) ([]Needed, error) {
raw, ok := xpak["NEEDED.ELF.2"]
if !ok {
// Not all packages have shared objects
return nil, nil
}
content := string(raw)
lines := strings.Split(content, "\n")
items := make([]Needed, 0, len(lines))
for _, line := range lines {
if line == "" {
continue
}
item := Needed{}
v := strings.Split(line, ";")
if len(v) < 5 {
return nil, fmt.Errorf("Expected at least 5 columns, got %d: %s", len(v), line)
}
item.arch = v[0]
item.absLibPath = v[1]
item.soname = v[2]
item.rpaths = strings.Split(v[3], ":")
item.needed = strings.Split(v[4], ",")
if len(v) > 5 {
// This field is optional
item.multilib = v[5]
}
items = append(items, item)
}
return items, nil
}
func collectContents(binPkg *binarypackage.File) ([]tar.FileListItem, error) {
reader, err := binPkg.TarballReader()
if err != nil {
return nil, err
}
defer reader.Close()
return tar.ListFilesZstd(reader)
}
func extractMetadata(ctx context.Context, fileName string) (*packageMetadata, error) {
binPkg, err := binarypackage.BinaryPackage(fileName)
if err != nil {
return nil, fmt.Errorf("failed opening binpkg: %w", err)
}
defer binPkg.Close()
xpakHeader, err := binPkg.Xpak()
if err != nil {
return nil, err
}
pkgNameBytes, ok := xpakHeader["PF"]
if !ok {
return nil, fmt.Errorf("failed to read PF key: %s", fileName)
}
pkgName := strings.TrimSpace(string(pkgNameBytes))
categoryBytes, ok := xpakHeader["CATEGORY"]
if !ok {
return nil, fmt.Errorf("failed to read CATEGORY key: %s", fileName)
}
category := strings.TrimSpace(string(categoryBytes))
atom, err := dependency.ParseAtom(fmt.Sprintf("=%s/%s", category, pkgName))
if err != nil {
return nil, err
}
overlayBytes, ok := xpakHeader["repository"]
if !ok {
return nil, fmt.Errorf("failed to read repository key: %s", fileName)
}
overlay := strings.TrimSpace(string(overlayBytes))
needed, err := parseNeeded(xpakHeader)
if err != nil {
return nil, err
}
contents, err := collectContents(binPkg)
if err != nil {
return nil, err
}
metadata, err := newPackageMetadata(overlay, atom, needed, contents)
if err != nil {
return nil, err
}
return metadata, nil
}
var metadataTemplate = template.Must(template.New("").Parse(`# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This file was generated using the following command. Please remove this
# message if the file was manually updated.
# $ bazel run //bazel/ebuild/cmd/xpak:xpak -- extract-metadata {{ .BinPkgFileName }}
#
# When would I manually update this file? If some of the outputs or deps are
# dependent on a USE flag. The generator doesn't have a way of extracting that
# data so it needs to be manually entered.
#
# i.e.,
# OUT_HEADERS="extra?( /usr/include/extra.h )"
#
# Why do we need this file? It's not used by portage, but by our ebuild -> bazel
# BUILD file generator. It allows us to generate an optimized build graph.
#
{{- if .Headers }}
OUT_HEADERS="
{{- range .Headers }}
{{ . }}
{{- end }}
"
{{- end }}
{{- if .PkgConfig }}
OUT_PKG_CONFIG="
{{- range .PkgConfig }}
{{ . }}
{{- end }}
"
{{- end }}
{{- if .StaticLibs }}
OUT_STATIC_LIBS="
{{- range .StaticLibs }}
{{ . }}
{{- end }}
"
{{- end }}
{{- if .SharedLibs }}
OUT_SHARED_LIBS="
{{- range .SharedLibs }}
{{ . }}
{{- end }}
"
{{- end }}
{{- if .SharedLibDeps }}
# The .so's required by all the shared libraries that are produced by this
# package. These will be included as transitive dependencies for all of reverse
# dependencies of this package.
OUT_SHARED_LIB_DEPS="
{{- range .SharedLibDeps }}
{{ . }}
{{- end }}
"
{{- end }}
{{- if .BinLibDeps }}
# The .so's required by all the executable binaries that are produced by this
# package. These will only be included as runtime dependencies (i.e., not build
# time dependencies) by the reverse dependencies of this package.
#
# If a dependency is specified in OUT_SHARED_LIB_DEPS, it shouldn't be listed
# here.
OUT_BIN_LIB_DEPS="
{{- range .BinLibDeps }}
{{ . }}
{{- end }}
"
{{- end }}
`))
func writeMetadataTemplate(ebuild string, m *packageMetadata) error {
out, err := os.Create(ebuild + ".ext")
if err != nil {
return err
}
defer out.Close()
return metadataTemplate.Execute(out, m)
}
func extractMetadataCmd(ctx context.Context, write bool, fileNames []string) error {
for _, fileName := range fileNames {
m, err := extractMetadata(ctx, fileName)
if err != nil {
return err
}
fmt.Printf("%s\n", m.EbuildPath())
if len(m.Headers()) > 0 {
fmt.Printf(" Headers: %s\n", m.Headers())
}
if len(m.PkgConfig()) > 0 {
fmt.Printf(" PkgConfig: %s\n", m.PkgConfig())
}
if len(m.StaticLibs()) > 0 {
fmt.Printf(" StaticLibs: %s\n", m.StaticLibs())
}
if len(m.SharedLibs()) > 0 {
fmt.Printf(" SharedLibs: %s\n", m.SharedLibs())
}
if len(m.SharedLibDeps()) > 0 {
fmt.Printf(" SharedLibDeps: %s\n", m.SharedLibDeps())
}
if len(m.BinLibDeps()) > 0 {
fmt.Printf(" BinLibDeps: %s\n", m.BinLibDeps())
}
ebuild := filepath.Join(bazelutil.WorkspaceDir(), m.EbuildPath())
if m.IsEmpty() {
// TODO: Should we delete the metadata file if it exists?
continue
}
if _, err := os.Stat(ebuild); err != nil {
if write {
return fmt.Errorf("ebuild %s: %w", ebuild, err)
} else {
// Just issue a warning
fmt.Printf("ebuild %s: %v", ebuild, err)
continue
}
}
if err = writeMetadataTemplate(ebuild, m); err != nil {
return err
}
}
return nil
}