* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain n copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
******************************************************************/
package build
import (
"context"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/strslice"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/docker"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
"gopkg.openfuyao.cn/bkeadm/pkg/infrastructure"
"gopkg.openfuyao.cn/bkeadm/pkg/root"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
type RpmOptions struct {
root.Options
Source string `json:"source"`
Add string `json:"add"`
Registry string `json:"registry"`
Package string `json:"package"`
}
const (
addListMinParts = 3
)
var adds = map[string]string{
"centos/7/amd64": "CentOS/7/amd64",
"centos/7/arm64": "CentOS/7/arm64",
"centos/8/amd64": "CentOS/8/amd64",
"centos/8/arm64": "CentOS/8/arm64",
"ubuntu/22/amd64": "Ubuntu/22/amd64",
"ubuntu/22/arm64": "Ubuntu/22/arm64",
"kylin/v10/arm64": "Kylin/V10/arm64",
"kylin/v10/amd64": "Kylin/V10/amd64",
}
func compressAndCleanupRpm(targetFile string, successMsg string) error {
if err := os.RemoveAll(targetFile); err != nil {
log.Errorf("Failed to remove file %s %v", targetFile, err)
return err
}
if err := global.TarGZ(packages, targetFile); err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
if err := os.Chmod(targetFile, utils.DefaultDirPermission); err != nil {
log.Warnf("Failed to chmod %s: %v", targetFile, err)
}
if err := os.RemoveAll(packages); err != nil {
log.Errorf("Failed to remove file %s %v", packages, err)
return err
}
log.Infof("%s %s", successMsg, targetFile)
return nil
}
func cleanRepodata(mnt string) error {
entries, err := os.ReadDir(mnt)
if err != nil {
log.Errorf("Failed to read the directory, %v", err)
return err
}
if err := os.RemoveAll(path.Join(mnt, "repodata")); err != nil {
log.Warnf("Failed to remove repodata: %v", err)
}
if err := os.RemoveAll(path.Join(mnt, ".repodata")); err != nil {
log.Warnf("Failed to remove .repodata: %v", err)
}
for _, entry := range entries {
if err := os.RemoveAll(path.Join(mnt, entry.Name(), "repodata")); err != nil {
log.Warnf("Failed to remove %s/repodata: %v", entry.Name(), err)
}
if err := os.RemoveAll(path.Join(mnt, ".repodata")); err != nil {
log.Warnf("Failed to remove .repodata: %v", err)
}
}
return nil
}
func runBuildContainer(image, mnt, containerName, cmd string) error {
return global.Docker.Run(
&container.Config{
Image: image,
WorkingDir: "/opt/mnt",
Cmd: strslice.StrSlice{
"sh",
"-c",
cmd,
},
},
&container.HostConfig{
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: mnt,
Target: "/opt/mnt",
},
},
RestartPolicy: container.RestartPolicy{
Name: "no",
MaximumRetryCount: 0,
},
}, nil, nil, containerName)
}
func waitForContainerComplete(containerName string) {
for {
time.Sleep(utils.ContainerWaitSeconds * time.Second)
containerInfo, _ := global.Docker.GetClient().ContainerInspect(context.TODO(), containerName)
if containerInfo.ContainerJSONBase != nil {
if !containerInfo.State.Running {
break
}
} else {
break
}
}
}
func ensureRpmBuildImage(registry, imageTag string) (string, error) {
image := registry + "/" + imageTag
err := global.Docker.EnsureImageExists(docker.ImageRef{
Image: image,
}, utils.RetryOptions{MaxRetry: 3, Delay: 1})
if err != nil {
log.Errorf("Failed to pull the mirror %v", err)
return "", err
}
return image, nil
}
func executeRpmBuildContainer(image, mnt, containerName, cmd string) error {
_ = global.Docker.ContainerRemove(containerName)
err := runBuildContainer(image, mnt, containerName, cmd)
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
defer func() {
_ = global.Docker.ContainerRemove(containerName)
}()
waitForContainerComplete(containerName)
return nil
}
func verifyRpmBuildResult(mnt, osInfo string, requiredFiles ...string) error {
for _, file := range requiredFiles {
if !utils.Exists(path.Join(mnt, file)) {
log.Errorf("Failed to build the %s %s rpm package", osInfo, mnt)
return fmt.Errorf("%s not found", file)
}
}
return nil
}
type rpmBuildConfig struct {
registry string
mnt string
image string
containerName string
cmd string
checkFile string
osInfo string
}
func validateAddOption(add string) bool {
_, ok := adds[add]
return ok
}
func validatePackageDirectory(pack string, add string) error {
if !utils.IsDir(pack) {
return fmt.Errorf("the %s is not a directory", pack)
}
entries, err := os.ReadDir(pack)
if err != nil {
return fmt.Errorf("failed to read the directory, %v", err)
}
for _, entry := range entries {
if strings.HasPrefix(add, "centos") && entry.Name() == "modules.yaml" {
continue
}
if strings.HasPrefix(add, "ubuntu") && entry.Name() == "Packages.gz" {
continue
}
if !entry.IsDir() {
return fmt.Errorf("the %s/%s is not a directory", pack, entry.Name())
}
}
return nil
}
func getAbsolutePath(path string) (string, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("failed to get the absolute path, %v", err)
}
return absPath, nil
}
func (ro *RpmOptions) Build() {
if len(ro.Source) == 0 && len(ro.Add) == 0 && len(ro.Package) == 0 {
consoleOutputStruct()
return
}
if len(ro.Add) != 0 {
ro.Add = strings.ToLower(ro.Add)
if !validateAddOption(ro.Add) {
log.Errorf("The %s is not a valid add. ", ro.Add)
log.Info("Valid add: centos/7/amd64, centos/7/arm64, centos/8/amd64, centos/8/arm64, "+
"ubuntu/22/amd64, ubuntu/22/arm64, kylin/v10/arm64, kylin/v10/amd64")
return
}
}
if len(ro.Package) != 0 {
if err := validatePackageDirectory(ro.Package, ro.Add); err != nil {
log.Errorf("Validate package directory %s failed: %v", ro.Package, err)
return
}
absPath, err := getAbsolutePath(ro.Package)
if err != nil {
log.Errorf("Get absolute package path %s failed: %v", ro.Package, err)
return
}
ro.Package = absPath
}
if len(ro.Source) != 0 {
absPath, err := getAbsolutePath(ro.Source)
if err != nil {
log.Errorf("Get absolute source path %s failed: %v", ro.Source, err)
return
}
ro.Source = absPath
}
if !infrastructure.IsDocker() {
log.Error("This build instruction only supports running in docker environment.")
return
}
ro.executeBuild()
}
func (ro *RpmOptions) executeBuild() {
if len(ro.Source) == 0 {
if err := rmpBuild(ro.Registry, ro.Add, ro.Package); err != nil {
log.Errorf("Failed to build rpm, %v", err)
}
return
}
if len(ro.Source) != 0 && len(ro.Add) == 0 && len(ro.Package) == 0 {
rpmBuildPackage(ro.Source, ro.Registry)
return
}
if len(ro.Source) != 0 && len(ro.Add) != 0 && len(ro.Package) != 0 {
rpmPackageAddOne(ro.Source, ro.Registry, ro.Add, ro.Package)
return
}
}
func prepareWorkspace() error {
pathLevel := []string{
packages,
path.Join(packages, "files"),
path.Join(packages, "CentOS", "7", "amd64"),
path.Join(packages, "CentOS", "7", "arm64"),
path.Join(packages, "CentOS", "8", "amd64"),
path.Join(packages, "CentOS", "8", "arm64"),
path.Join(packages, "Ubuntu", "22", "amd64"),
path.Join(packages, "Ubuntu", "22", "arm64"),
path.Join(packages, "Kylin", "V10", "amd64"),
path.Join(packages, "Kylin", "V10", "arm64"),
}
if err := os.RemoveAll(packages); err != nil {
log.Errorf("Unable to remove file %s %v", packages, err)
return err
}
for _, pl := range pathLevel {
if err := os.MkdirAll(pl, utils.DefaultDirPermission); err != nil {
log.Errorf("Unable to create file %s %v", pl, err)
return err
}
}
return nil
}
func consoleOutputStruct() {
content := `rpm
├── CentOS
│ ├── 7
│ │ ├── amd64
│ │ └── arm64
│ └── 8
│ ├── amd64
│ └── arm64
├── files
├── Kylin
│ └── V10
│ ├── amd64
│ └── arm64
└── Ubuntu
└── 22
├── amd64
└── arm64
`
fmt.Print(content)
}
func rmpBuild(registry string, add string, absPath string) error {
switch add {
case "centos/7/amd64", "centos/7/arm64":
err := rpmCentos7Build(registry, absPath)
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
case "centos/8/amd64", "centos/8/arm64":
err := rpmCentos8Build(registry, absPath)
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
case "ubuntu/22/amd64", "ubuntu/22/arm64":
err := rpmUbuntu22Build(registry, absPath)
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
case "kylin/v10/amd64", "kylin/v10/arm64":
err := rpmKylinV10Build(registry, absPath)
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
default:
log.Errorf("The %s is not supported", add)
return errors.New("not supported")
}
log.Infof("Successfully build rpm %s", add)
return nil
}
type target struct {
osName string
version string
arch string
builder func(string, string) error
}
func getTargets() []target {
return []target{
{"Centos", "7", "amd64", rpmCentos7Build},
{"Centos", "7", "arm64", rpmCentos7Build},
{"Centos", "8", "amd64", rpmCentos8Build},
{"Centos", "8", "arm64", rpmCentos8Build},
{"Ubuntu", "22", "amd64", rpmUbuntu22Build},
{"Ubuntu", "22", "arm64", rpmUbuntu22Build},
{"Kylin", "V10", "amd64", rpmKylinV10Build},
{"Kylin", "V10", "arm64", rpmKylinV10Build},
}
}
func executeSingleTarget(registry string, t target) error {
targetPath := path.Join(packages, t.osName, t.version, t.arch)
return t.builder(registry, targetPath)
}
func rpmBuildAllArchitectures(registry string) error {
targets := getTargets()
for _, t := range targets {
if err := executeSingleTarget(registry, t); err != nil {
return fmt.Errorf("failed to build rpm for %s/%s/%s: %w",
t.osName, t.version, t.arch, err)
}
}
return nil
}
func rpmBuildPackage(source string, registry string) {
if !utils.IsDir(source) {
log.Errorf("The %s is not a directory. ", source)
return
}
if err := prepareWorkspace(); err != nil {
log.Errorf("Prepare workspace failed: %v", err)
return
}
if err := utils.CopyDir(source, packages); err != nil {
log.Errorf("Copy rpm source %s to %s failed: %v", source, packages, err)
return
}
if err := rpmBuildAllArchitectures(registry); err != nil {
log.Errorf("Build rpm for all architectures failed: %v", err)
return
}
if err := compressAndCleanupRpm(path.Join(pwd, "rpm.tar.gz.1"), "Build rpm success."); err != nil {
return
}
}
func rpmPackageAddOne(source string, registry string, add string, pack string) {
if !utils.IsFile(source) {
log.Errorf("The %s is not a file. ", source)
return
}
err := os.RemoveAll(packages)
if err != nil {
log.Errorf("Failed to remove file %s %v", packages, err)
return
}
err = os.MkdirAll(packages, utils.DefaultDirPermission)
if err != nil {
log.Errorf("Failed to create file %s %v", packages, err)
return
}
err = utils.UnTar(source, packages)
if err != nil {
log.Errorf("Failed to unTar %s %v", source, err)
return
}
addList := strings.Split(adds[add], "/")
if len(addList) < addListMinParts {
log.Errorf("Invalid add format, expected at least 3 parts separated by '/'")
return
}
err = utils.CopyDir(pack, path.Join(packages, addList[0], addList[1], addList[2]))
if err != nil {
log.Errorf("Failed to copy file %s %v", pack, err)
return
}
err = rmpBuild(registry, add, path.Join(packages, addList[0], addList[1], addList[2]))
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return
}
if err = compressAndCleanupRpm(
path.Join(pwd, "rpm.tar.gz.1"), "The rpm package has been successfully built"); err != nil {
return
}
}
func rpmCentos8Build(registry string, mnt string) error {
if utils.DirectoryIsEmpty(mnt) {
return nil
}
image, err := ensureRpmBuildImage(registry, "centos:8-amd64-build")
if err != nil {
return err
}
if err := cleanCentos8Modules(mnt); err != nil {
return err
}
cmd := "createrepo ./ && repo2module -s stable . modules.yaml && " +
"modifyrepo_c --mdtype=modules modules.yaml repodata/"
if err := executeRpmBuildContainer(image, mnt, "build-centos8-rpm", cmd); err != nil {
return err
}
return verifyRpmBuildResult(mnt, "centos/8/amd64", "modules.yaml", "repodata")
}
func cleanCentos8Modules(mnt string) error {
entries, err := os.ReadDir(mnt)
if err != nil {
log.Errorf("Failed to read the directory, %v", err)
return err
}
for _, f := range []string{"modules.yaml", "repodata", ".repodata"} {
if err := os.RemoveAll(path.Join(mnt, f)); err != nil {
log.Warnf("Failed to remove %s: %v", f, err)
}
}
for _, entry := range entries {
for _, f := range []string{"modules.yaml", "repodata"} {
if err := os.RemoveAll(path.Join(mnt, entry.Name(), f)); err != nil {
log.Warnf("Failed to remove %s/%s: %v", entry.Name(), f, err)
}
}
}
return nil
}
func executeGenericRpmBuild(cfg rpmBuildConfig) error {
if utils.DirectoryIsEmpty(cfg.mnt) {
return nil
}
image, err := ensureRpmBuildImage(cfg.registry, cfg.image)
if err != nil {
return err
}
if err := cleanRepodata(cfg.mnt); err != nil {
return err
}
if err := executeRpmBuildContainer(image, cfg.mnt, cfg.containerName, cfg.cmd); err != nil {
return err
}
return verifyRpmBuildResult(cfg.mnt, cfg.osInfo, cfg.checkFile)
}
func rpmCentos7Build(registry string, mnt string) error {
return executeGenericRpmBuild(rpmBuildConfig{
registry: registry,
mnt: mnt,
image: "centos:7-amd64-build",
containerName: "build-centos7-rpm",
cmd: "createrepo ./",
osInfo: "centos/7/amd64",
checkFile: "repodata",
})
}
func rpmUbuntu22Build(registry string, mnt string) error {
if utils.DirectoryIsEmpty(mnt) {
return nil
}
image := registry + "/ubuntu:22-amd64-build"
err := global.Docker.EnsureImageExists(docker.ImageRef{
Image: image,
}, utils.RetryOptions{MaxRetry: 3, Delay: 1})
if err != nil {
log.Errorf("Failed to pull the mirror %v", err)
return err
}
if err := os.RemoveAll(path.Join(mnt, "Packages.gz")); err != nil {
log.Warnf("Failed to remove Packages.gz: %v", err)
}
if err := os.RemoveAll(path.Join(mnt, "archives", "Packages.gz")); err != nil {
log.Warnf("Failed to remove archives/Packages.gz: %v", err)
}
name := "build-ubuntu22-rpm"
_ = global.Docker.ContainerRemove(name)
err = runBuildContainer(image, mnt, name,
"dpkg-scanpackages -m . /dev/null | gzip -9c > Packages.gz && cp Packages.gz ./archives")
if err != nil {
log.Errorf("Failed to build rpm %v", err)
return err
}
defer func() {
_ = global.Docker.ContainerRemove(name)
}()
waitForContainerComplete(name)
if !utils.Exists(path.Join(mnt, "Packages.gz")) {
log.Errorf("Failed to build the ubuntu/22/amd64 %s rpm package", mnt)
return errors.New("packages.gz not found")
}
return nil
}
func rpmKylinV10Build(registry string, mnt string) error {
return executeGenericRpmBuild(rpmBuildConfig{
registry: registry,
mnt: mnt,
image: "centos:7-amd64-build",
containerName: "build-kylin10-rpm",
cmd: "createrepo ./",
osInfo: "kylin/v10/amd64",
checkFile: "repodata",
})
}