Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package melange for openSUSE:Factory checked in at 2025-05-13 20:06:06 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Tue May 13 20:06:06 2025 rev:85 rq:1276954 version:0.23.17 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2025-05-12 16:54:50.607697472 +0200 +++ /work/SRC/openSUSE:Factory/.melange.new.30101/melange.changes 2025-05-13 20:06:09.568570769 +0200 @@ -1,0 +2,6 @@ +Mon May 12 19:02:12 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- Update to version 0.23.17: + * experiment: on the fly disk for melange QEMU runner (#1977) + +------------------------------------------------------------------- Old: ---- melange-0.23.16.obscpio New: ---- melange-0.23.17.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.CpuqtI/_old 2025-05-13 20:06:11.136636737 +0200 +++ /var/tmp/diff_new_pack.CpuqtI/_new 2025-05-13 20:06:11.140636905 +0200 @@ -17,7 +17,7 @@ Name: melange -Version: 0.23.16 +Version: 0.23.17 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.CpuqtI/_old 2025-05-13 20:06:11.168638083 +0200 +++ /var/tmp/diff_new_pack.CpuqtI/_new 2025-05-13 20:06:11.172638251 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/melange</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.23.16</param> + <param name="revision">v0.23.17</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.CpuqtI/_old 2025-05-13 20:06:11.192639093 +0200 +++ /var/tmp/diff_new_pack.CpuqtI/_new 2025-05-13 20:06:11.196639261 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/melange</param> - <param name="changesrevision">5b4c1e4b78a5373233946c0d9c81442f85f45dad</param></service></servicedata> + <param name="changesrevision">76a4bd045fcad1727125b4ee7c12e97cf145c27c</param></service></servicedata> (No newline at EOF) ++++++ melange-0.23.16.obscpio -> melange-0.23.17.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/build/build.go new/melange-0.23.17/pkg/build/build.go --- old/melange-0.23.16/pkg/build/build.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/build/build.go 2025-05-12 19:42:00.000000000 +0200 @@ -16,7 +16,6 @@ import ( "archive/tar" - "compress/gzip" "context" "encoding/hex" "encoding/json" @@ -309,8 +308,11 @@ defer os.RemoveAll(tmp) if b.Runner.Name() == container.QemuName { + /* + * here we need to inject gnutar+attr in order to syphon back + * the workspace from the VM preserving extended attributes. + */ b.ExtraPackages = append(b.ExtraPackages, []string{ - "melange-microvm-init", "gnutar", "attr", }...) @@ -1060,6 +1062,47 @@ return b.Libc } +func runAsUID(accts apko_types.ImageAccounts) string { + switch accts.RunAs { + case "": + return "" // Runner defaults + case "root", "0": + return "0" + default: + } + // If accts.RunAs is numeric, then return it. + if _, err := strconv.Atoi(accts.RunAs); err == nil { + return accts.RunAs + } + for _, u := range accts.Users { + if accts.RunAs == u.UserName { + return fmt.Sprint(u.UID) + } + } + panic(fmt.Sprintf("unable to find user with username %s", accts.RunAs)) +} + +func runAs(accts apko_types.ImageAccounts) string { + switch accts.RunAs { + case "": + return "" // Runner defaults + case "root", "0": + return "root" + default: + } + // If accts.RunAs is numeric, then look up the username. + uid, err := strconv.Atoi(accts.RunAs) + if err != nil { + return accts.RunAs + } + for _, u := range accts.Users { + if u.UID == uint32(uid) { + return u.UserName + } + } + panic(fmt.Sprintf("unable to find user with UID %d", uid)) +} + func (b *Build) buildWorkspaceConfig(ctx context.Context) *container.Config { log := clog.FromContext(ctx) @@ -1099,7 +1142,8 @@ }, WorkspaceDir: b.WorkspaceDir, Timeout: b.Configuration.Package.Timeout, - RunAs: b.Configuration.Environment.Accounts.RunAs, + RunAsUID: runAsUID(b.Configuration.Environment.Accounts), + RunAs: runAs(b.Configuration.Environment.Accounts), } if b.Configuration.Package.Resources != nil { @@ -1137,7 +1181,14 @@ ctx, span := otel.Tracer("melange").Start(ctx, "retrieveWorkspace") defer span.End() - r, err := b.Runner.WorkspaceTar(ctx, b.containerConfig) + extraFiles := []string{} + for _, v := range b.Configuration.Package.Copyright { + if v.LicensePath != "" { + extraFiles = append(extraFiles, v.LicensePath) + } + } + + r, err := b.Runner.WorkspaceTar(ctx, b.containerConfig, extraFiles) if err != nil { return err } else if r == nil { @@ -1145,12 +1196,7 @@ } defer r.Close() - gr, err := gzip.NewReader(r) - if err != nil { - return err - } - defer gr.Close() - tr := tar.NewReader(gr) + tr := tar.NewReader(r) for { hdr, err := tr.Next() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/build/test.go new/melange-0.23.17/pkg/build/test.go --- old/melange-0.23.16/pkg/build/test.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/build/test.go 2025-05-12 19:42:00.000000000 +0200 @@ -278,13 +278,6 @@ return fmt.Errorf("compiling %s tests: %w", t.ConfigFile, err) } - if t.Runner.Name() == container.QemuName { - t.ExtraTestPackages = append(t.ExtraTestPackages, []string{ - "melange-microvm-init", - "gnutar", - }...) - } - // Filter out any subpackages with false If conditions. t.Configuration.Subpackages = slices.DeleteFunc(t.Configuration.Subpackages, func(sp config.Subpackage) bool { result, err := shouldRun(sp.If) @@ -501,7 +494,8 @@ Capabilities: caps, WorkspaceDir: t.WorkspaceDir, Environment: map[string]string{}, - RunAs: imgcfg.Accounts.RunAs, + RunAsUID: runAsUID(imgcfg.Accounts), + RunAs: runAs(imgcfg.Accounts), } if t.Configuration.Capabilities.Add != nil { cfg.Capabilities.Add = t.Configuration.Capabilities.Add diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/bubblewrap_runner.go new/melange-0.23.17/pkg/container/bubblewrap_runner.go --- old/melange-0.23.16/pkg/container/bubblewrap_runner.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/bubblewrap_runner.go 2025-05-12 19:42:00.000000000 +0200 @@ -116,10 +116,10 @@ "--clearenv") // If we need to run as an user, we run as that user. - if cfg.RunAs != "" { + if cfg.RunAsUID != "" { baseargs = append(baseargs, "--unshare-user") - baseargs = append(baseargs, "--uid", cfg.RunAs) - baseargs = append(baseargs, "--gid", cfg.RunAs) + baseargs = append(baseargs, "--uid", cfg.RunAsUID) + baseargs = append(baseargs, "--gid", cfg.RunAsUID) // Else if we're not using melange as root, we force the use of the // Apko build user. This avoids problems on machines where default // regular user is NOT 1000. @@ -226,7 +226,7 @@ // WorkspaceTar implements Runner // This is a noop for Bubblewrap, which uses bind-mounts to manage the workspace -func (bw *bubblewrap) WorkspaceTar(ctx context.Context, cfg *Config) (io.ReadCloser, error) { +func (bw *bubblewrap) WorkspaceTar(ctx context.Context, cfg *Config, extraFiles []string) (io.ReadCloser, error) { return nil, nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/bubblewrap_runner_test.go new/melange-0.23.17/pkg/container/bubblewrap_runner_test.go --- old/melange-0.23.16/pkg/container/bubblewrap_runner_test.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/bubblewrap_runner_test.go 2025-05-12 19:42:00.000000000 +0200 @@ -35,7 +35,7 @@ }, { name: "With config RunAs", - config: &Config{RunAs: "65535"}, + config: &Config{RunAsUID: "65535"}, expectedArgs: fmt.Sprintf("--unshare-user --uid %s --gid %s", "65535", "65535"), }, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/config.go new/melange-0.23.17/pkg/container/config.go --- old/melange-0.23.16/pkg/container/config.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/config.go 2025-05-12 19:42:00.000000000 +0200 @@ -52,6 +52,7 @@ ImgRef string PodID string Arch apko_types.Architecture + RunAsUID string RunAs string WorkspaceDir string CPU, CPUModel, Memory string diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/docker/docker_runner.go new/melange-0.23.17/pkg/container/docker/docker_runner.go --- old/melange-0.23.16/pkg/container/docker/docker_runner.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/docker/docker_runner.go 2025-05-12 19:42:00.000000000 +0200 @@ -220,7 +220,7 @@ } taskIDResp, err := dk.cli.ContainerExecCreate(ctx, cfg.PodID, container.ExecOptions{ - User: cfg.RunAs, + User: cfg.RunAsUID, Cmd: args, WorkingDir: runnerWorkdir, Env: environ, @@ -358,7 +358,7 @@ // WorkspaceTar implements Runner // This is a noop for Docker, which uses bind-mounts to manage the workspace -func (dk *docker) WorkspaceTar(ctx context.Context, cfg *mcontainer.Config) (io.ReadCloser, error) { +func (dk *docker) WorkspaceTar(ctx context.Context, cfg *mcontainer.Config, extraFiles []string) (io.ReadCloser, error) { return nil, nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/qemu_runner.go new/melange-0.23.17/pkg/container/qemu_runner.go --- old/melange-0.23.16/pkg/container/qemu_runner.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/qemu_runner.go 2025-05-12 19:42:00.000000000 +0200 @@ -18,7 +18,6 @@ "archive/tar" "bufio" "bytes" - "compress/gzip" "context" "crypto/ecdsa" "crypto/elliptic" @@ -41,11 +40,13 @@ "syscall" "time" + apkofs "chainguard.dev/apko/pkg/apk/fs" apko_build "chainguard.dev/apko/pkg/build" apko_types "chainguard.dev/apko/pkg/build/types" apko_cpio "chainguard.dev/apko/pkg/cpio" "chainguard.dev/melange/internal/logwriter" "github.com/chainguard-dev/clog" + "github.com/charmbracelet/log" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/kballard/go-shellquote" @@ -86,11 +87,10 @@ defer stdout.Close() defer stderr.Close() - // default to root user but if a different user is specified - // we will use the embedded build:1000:1000 user + // default to root user, unless a different user is specified user := "root" if cfg.RunAs != "" { - user = "build" + user = cfg.RunAs } err := sendSSHCommand(ctx, @@ -114,13 +114,6 @@ func (bw *qemu) Debug(ctx context.Context, cfg *Config, envOverride map[string]string, args ...string) error { clog.InfoContextf(ctx, "debugging command %s", strings.Join(args, " ")) - // default to root user but if a different user is specified - // we will use the embedded build:1000:1000 user - user := "root" - if cfg.RunAs != "" { - user = "build" - } - // handle terminal size, resizing and sigwinch to keep // it updated fd := int(os.Stdin.Fd()) @@ -152,6 +145,12 @@ return err } + // default to root user, unless a different user is specified + user := "root" + if cfg.RunAs != "" { + user = cfg.RunAs + } + // Create SSH client configuration config := &ssh.ClientConfig{ User: user, @@ -159,7 +158,7 @@ ssh.PublicKeys(signer), }, Config: ssh.Config{ - Ciphers: []string{"aes128-ctr", "aes256-...@openssh.com"}, + Ciphers: []string{"aes128-...@openssh.com"}, }, HostKeyCallback: hostKeyCallback, } @@ -371,15 +370,8 @@ } // WorkspaceTar implements Runner -func (bw *qemu) WorkspaceTar(ctx context.Context, cfg *Config) (io.ReadCloser, error) { - // default to root user but if a different user is specified - // we will use the embedded build:1000:1000 user - user := "root" - if cfg.RunAs != "" { - user = "build" - } - - outFile, err := os.Create(filepath.Join(cfg.WorkspaceDir, "melange-out.tar.gz")) +func (bw *qemu) WorkspaceTar(ctx context.Context, cfg *Config, extraFiles []string) (io.ReadCloser, error) { + outFile, err := os.Create(filepath.Join(cfg.WorkspaceDir, "melange-out.tar")) if err != nil { return nil, err } @@ -393,6 +385,19 @@ // We could just cp -a to /mnt as it is our shared workspace directory, but // this will lose some file metadata like hardlinks, owners and so on. // Example of package that won't work when using "cp -a" is glibc. + retrieveCommand := "cd /home/build && tar cvpf - --xattrs --acls --exclude='*fifo*' melange-out" + // we append also all the necessary files that we might need, for example Licenses + // for license checks + for _, v := range extraFiles { + retrieveCommand = retrieveCommand + " " + v + } + + // default to root user, unless a different user is specified + user := "root" + if cfg.RunAs != "" { + user = cfg.RunAs + } + err = sendSSHCommand(ctx, user, cfg.SSHAddress, @@ -402,7 +407,7 @@ nil, outFile, false, - []string{"sh", "-c", "cd /home/build && tar cvpzf - --xattrs --acls --exclude='*fifo*' *"}, + []string{"sh", "-c", retrieveCommand}, ) if err != nil { return nil, err @@ -418,42 +423,23 @@ defer span.End() // qemu does not have the idea of container images or layers or such, just - // create an initramfs from the layer - guestInitramfs, err := os.CreateTemp("", "melange-guest-*.initramfs.cpio") + // create a rootfs from the layer + guestRootfs, err := os.CreateTemp("", "melange-guest-*.tar") if err != nil { clog.FromContext(ctx).Errorf("failed to create guest dir: %v", err) return ref, err } - // in case of some kernel images, we also need the /lib/modules directory to load - // necessary drivers, like 9p, virtio_net which are foundamental for the VM working. - if qemuModule, ok := os.LookupEnv("QEMU_KERNEL_MODULES"); ok { - clog.FromContext(ctx).Debugf("qemu: QEMU_KERNEL_MODULES env set, injecting modules in initramfs") - if _, err := os.Stat(qemuModule); err == nil { - clog.FromContext(ctx).Debugf("qemu: local QEMU_KERNEL_MODULES dir detected, injecting") - layer, err = injectKernelModules(ctx, layer, qemuModule) - if err != nil { - clog.FromContext(ctx).Errorf("qemu: could not inject needed kernel modules into initramfs: %v", err) - return "", err - } - } - } - - // We see issues with qemu launching the initrd when the size of the - // uncompressed CPIO exceeds ~2G and change (very suspiciously around - // max signed int32), so take the performance hit of compressing the initrd - // (a double hit b/c the kernel will decompress). - gzw := gzip.NewWriter(guestInitramfs) - if err := apko_cpio.FromLayer(layer, gzw); err != nil { - clog.FromContext(ctx).Errorf("failed to create cpio initramfs: %v", err) - return ref, err - } - if err := gzw.Close(); err != nil { - clog.FromContext(ctx).Errorf("failed to close gzip writer: %v", err) - return ref, err + // the uncompressed layer will be unpacked in a rootfs by the + // initramfs later + layerUncompress, err := layer.Uncompressed() + if err != nil { + return "", err } + _, err = io.Copy(guestRootfs, layerUncompress) + guestRootfs.Close() - return guestInitramfs.Name(), nil + return guestRootfs.Name(), nil } func (b qemuOCILoader) RemoveImage(ctx context.Context, ref string) error { @@ -469,13 +455,6 @@ return err } - // always be sure to create the VM rootfs first! - kernelPath, rootfsInitrdPath, err := getKernelPath(ctx, cfg) - if err != nil { - clog.FromContext(ctx).Errorf("could not prepare rootfs: %v", err) - return err - } - baseargs := []string{} bios := false useVM := false @@ -535,9 +514,6 @@ } } - baseargs = append(baseargs, "-kernel", kernelPath) - baseargs = append(baseargs, "-initrd", rootfsInitrdPath) - if cfg.Memory != "" { memKb, err := convertHumanToKB(cfg.Memory) if err != nil { @@ -607,7 +583,7 @@ // panic=-1 ensures that if the init fails, we immediately exit the machine // Add default SSH keys to the VM sshkey := base64.StdEncoding.EncodeToString(pubKey) - baseargs = append(baseargs, "-append", kernelConsole+" nomodeset panic=-1 sshkey="+sshkey) + baseargs = append(baseargs, "-append", kernelConsole+" nomodeset random.trust_cpu=on panic=-1 sshkey="+sshkey) // we will *not* mount workspace using qemu, this will use 9pfs which is network-based, and will // kill all performances (lots of small files) // instead we will copy back the finished workspace artifacts when done. @@ -621,6 +597,21 @@ cfg.Disk = defaultDiskSize } + kernelPath, err := getKernelPath(ctx, cfg) + if err != nil { + clog.FromContext(ctx).Errorf("could not prepare rootfs: %v", err) + return err + } + + initramFile, err := generateCpio(ctx) + if err != nil { + clog.FromContext(ctx).Errorf("qemu: could not generate initramfs: %v", err) + return err + } + + baseargs = append(baseargs, "-kernel", kernelPath) + baseargs = append(baseargs, "-initrd", initramFile) + // if we want a disk, just add it, the init will mount it to the build home automatically diskFile, err := generateDiskFile(ctx, cfg.Disk) if err != nil { @@ -628,12 +619,16 @@ return err } + // save the disk name, we will wipe it off when done + cfg.Disk = diskFile + // append raw disk, init will take care of formatting it if present. baseargs = append(baseargs, "-object", "iothread,id=io1") baseargs = append(baseargs, "-device", "virtio-blk-pci,drive=disk0,iothread=io1") baseargs = append(baseargs, "-drive", "if=none,id=disk0,cache=none,format=raw,aio=threads,werror=report,rerror=report,file="+diskFile) - // save the disk name, we will wipe it off when done - cfg.Disk = diskFile + // append the rootfs tar.gz, init will take care of populating the disk with it + baseargs = append(baseargs, "-device", "virtio-blk-pci,drive=image.tar,serial=input-tar,discard=true") + baseargs = append(baseargs, "-blockdev", "driver=raw,node-name=image.tar,file.driver=file,file.filename="+cfg.ImgRef) // qemu-system-x86_64 or qemu-system-aarch64... qemuCmd := exec.CommandContext(ctx, fmt.Sprintf("qemu-system-%s", cfg.Arch.ToAPK()), baseargs...) @@ -729,12 +724,12 @@ return fmt.Errorf("qemu: could not get VM host key") } - // default to root user but if a different user is specified - // we will use the embedded build:1000:1000 user + // default to root user, unless a different user is specified user := "root" if cfg.RunAs != "" { - user = "build" + user = cfg.RunAs } + clog.FromContext(ctx).Info("qemu: setting up local workspace") return sendSSHCommand(ctx, user, @@ -749,7 +744,7 @@ ) } -func getKernelPath(ctx context.Context, cfg *Config) (string, string, error) { +func getKernelPath(ctx context.Context, cfg *Config) (string, error) { clog.FromContext(ctx).Debug("qemu: setting up kernel for vm") kernel := "/boot/vmlinuz" if kernelVar, ok := os.LookupEnv("QEMU_KERNEL_IMAGE"); ok { @@ -759,86 +754,10 @@ kernel = kernelVar } } else if _, err := os.Stat(kernel); err != nil { - return "", "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a kernel path with env variable QEMU_KERNEL_IMAGE and QEMU_KERNEL_MODULES if needed") - } - - return kernel, cfg.ImgRef, nil -} - -// in case of external modules (usually for 9p and virtio) we need a matching /lib/modules/kernel-$(uname) -// we need to inject this directly into the initramfs cpio, as we cannot share them via 9p later. -func injectKernelModules(ctx context.Context, rootfs v1.Layer, modulesPath string) (v1.Layer, error) { - clog.FromContext(ctx).Info("qemu: appending modules to initramfs") - - // get tar layer, we will need to inject new files into it - uncompressed, err := rootfs.Uncompressed() - if err != nil { - return nil, err - } - defer uncompressed.Close() - - // copy old tar layer into new tar - buf := new(bytes.Buffer) - tarWriter := tar.NewWriter(buf) - tartReader := tar.NewReader(uncompressed) - - for { - header, err := tartReader.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if err := tarWriter.WriteHeader(header); err != nil { - return nil, err - } - if _, err := io.Copy(tarWriter, tartReader); err != nil { - return nil, err - } - } - - // Walk through the input directory and add files to the tar archive - err = filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - - data, err := os.ReadFile(path) - if err != nil { - return fmt.Errorf("failed to read file %s: %w", path, err) - } - - header, err := tar.FileInfoHeader(info, path) - if err != nil { - return fmt.Errorf("failed to create tar header for %s: %w", path, err) - } - - header.Name = "/lib/modules/" + filepath.ToSlash(path[len(modulesPath):]) - if err := tarWriter.WriteHeader(header); err != nil { - return fmt.Errorf("failed to write tar header for %s: %w", path, err) - } - - if _, err := tarWriter.Write(data); err != nil { - return fmt.Errorf("failed to write file %s to tar: %w", path, err) - } - - return nil - }) - if err != nil { - return nil, err - } - - opener := func() (io.ReadCloser, error) { - // Return a ReadCloser from the buffer - return io.NopCloser(bytes.NewReader(buf.Bytes())), nil + return "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a kernel path with env variable QEMU_KERNEL_IMAGE") } - // Create a layer from the Opener - return tarball.LayerFromOpener(opener) + return kernel, nil } func generateDiskFile(ctx context.Context, diskSize string) (string, error) { @@ -878,14 +797,14 @@ // this avoids the ssh client trying to connect on a booting server. func checkSSHServer(address string) error { // Establish a connection to the address - conn, err := net.DialTimeout("tcp", address, time.Second) + conn, err := net.DialTimeout("tcp", address, 50*time.Millisecond) if err != nil { return fmt.Errorf("dial: %w", err) } defer conn.Close() // Set a deadline for the connection - err = conn.SetDeadline(time.Now().Add(time.Second * 15)) + err = conn.SetDeadline(time.Now().Add(time.Second)) if err != nil { return err } @@ -914,14 +833,20 @@ return err } + // default to root user, unless a different user is specified + user := "root" + if cfg.RunAs != "" { + user = cfg.RunAs + } + // Create SSH client configuration config := &ssh.ClientConfig{ - User: "build", + User: user, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, Config: ssh.Config{ - Ciphers: []string{"aes128-ctr", "aes256-...@openssh.com"}, + Ciphers: []string{"aes128-...@openssh.com"}, }, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { hostKey = key @@ -982,7 +907,7 @@ ssh.PublicKeys(signer), }, Config: ssh.Config{ - Ciphers: []string{"aes128-ctr", "aes256-...@openssh.com"}, + Ciphers: []string{"aes128-...@openssh.com"}, }, HostKeyCallback: hostKeyCallback, } @@ -1040,7 +965,7 @@ clog.FromContext(ctx).Debugf("running (%d) %v", len(command), cmd) err = session.Run(cmd) if err != nil { - clog.FromContext(ctx).Errorf("Failed to run command: %v", err) + clog.FromContext(ctx).Errorf("Failed to run command %q as %q: %v", cmd, user, err) return err } @@ -1188,3 +1113,176 @@ return l.Addr().(*net.TCPAddr).Port, nil } + +func generateCpio(ctx context.Context) (string, error) { + /* + * we only build once, useful for local development, we + * cache it. + * if present, we nop and return, else we build it. + */ + cacheDir, err := os.UserCacheDir() + if err != nil { + cacheDir = filepath.Join( + "kernel", + apko_types.Architecture(runtime.GOARCH).ToAPK()) + + } + cacheDir = filepath.Join(cacheDir, "melange-cpio") + + initramfs := filepath.Join( + cacheDir, + "melange-guest.initramfs.cpio") + initramfsInfo, err := os.Stat(initramfs) + + // if file is presend and less than 24h old, then we just reuse it + if err == nil && time.Since(initramfsInfo.ModTime()) < 24*time.Hour { + return initramfs, nil + } + + clog.FromContext(ctx).Info("qemu: generating initramfs") + + err = os.MkdirAll(cacheDir, os.ModePerm) + if err != nil { + return "", fmt.Errorf("unable to dest directory: %w", err) + } + + spec := apko_types.ImageConfiguration{ + Contents: apko_types.ImageContents{ + BuildRepositories: []string{ + "https://apk.cgr.dev/chainguard", + }, + Packages: []string{ + "microvm-init", + }, + }, + } + opts := []apko_build.Option{apko_build.WithImageConfiguration(spec), + apko_build.WithArch(apko_types.Architecture(runtime.GOARCH)), + } + + tmpDir, err := os.MkdirTemp("", "melange-guest-*.initramfs") + if err != nil { + return "", fmt.Errorf("unable to create build context: %w", err) + } + defer os.RemoveAll(tmpDir) + + bc, err := apko_build.New(ctx, apkofs.DirFS(tmpDir, apkofs.WithCreateDir()), opts...) + if err != nil { + return "", fmt.Errorf("unable to create build context: %w", err) + } + + bc.Summarize(ctx) + if err := bc.BuildImage(ctx); err != nil { + return "", fmt.Errorf("unable to generate image: %w", err) + } + layerTarGZ, layer, err := bc.ImageLayoutToLayer(ctx) + if err != nil { + return "", err + } + defer os.Remove(layerTarGZ) + + log.Debugf("using %s for image layer", layerTarGZ) + + // in case of some kernel images, we also need the /lib/modules directory to load + // necessary drivers, like 9p, virtio_net which are foundamental for the VM working. + if qemuModule, ok := os.LookupEnv("QEMU_KERNEL_MODULES"); ok { + clog.FromContext(ctx).Debugf("qemu: QEMU_KERNEL_MODULES env set, injecting modules in initramfs") + if _, err := os.Stat(qemuModule); err == nil { + clog.FromContext(ctx).Debugf("qemu: local QEMU_KERNEL_MODULES dir detected, injecting") + layer, err = injectKernelModules(ctx, layer, qemuModule) + if err != nil { + clog.FromContext(ctx).Errorf("qemu: could not inject needed kernel modules into initramfs: %v", err) + return "", err + } + } + } + + guestInitramfs, err := os.Create(initramfs) + if err != nil { + clog.FromContext(ctx).Errorf("failed to create cpio initramfs: %v", err) + return "", err + } + + if err := apko_cpio.FromLayer(layer, guestInitramfs); err != nil { + clog.FromContext(ctx).Errorf("failed to convert cpio initramfs: %v", err) + return "", err + } + + return guestInitramfs.Name(), nil +} + +// in case of external modules (usually for 9p and virtio) we need a matching /lib/modules/kernel-$(uname) +// we need to inject this directly into the initramfs cpio, as we cannot share them via 9p later. +func injectKernelModules(ctx context.Context, rootfs v1.Layer, modulesPath string) (v1.Layer, error) { + clog.FromContext(ctx).Info("qemu: appending modules to initramfs") + + // get tar layer, we will need to inject new files into it + uncompressed, err := rootfs.Uncompressed() + if err != nil { + return nil, err + } + defer uncompressed.Close() + + // copy old tar layer into new tar + buf := new(bytes.Buffer) + tarWriter := tar.NewWriter(buf) + tartReader := tar.NewReader(uncompressed) + + for { + header, err := tartReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if err := tarWriter.WriteHeader(header); err != nil { + return nil, err + } + if _, err := io.Copy(tarWriter, tartReader); err != nil { + return nil, err + } + } + + // Walk through the input directory and add files to the tar archive + err = filepath.Walk(modulesPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", path, err) + } + + header, err := tar.FileInfoHeader(info, path) + if err != nil { + return fmt.Errorf("failed to create tar header for %s: %w", path, err) + } + + header.Name = "/lib/modules/" + filepath.ToSlash(path[len(modulesPath):]) + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write tar header for %s: %w", path, err) + } + + if _, err := tarWriter.Write(data); err != nil { + return fmt.Errorf("failed to write file %s to tar: %w", path, err) + } + + return nil + }) + if err != nil { + return nil, err + } + + opener := func() (io.ReadCloser, error) { + // Return a ReadCloser from the buffer + return io.NopCloser(bytes.NewReader(buf.Bytes())), nil + } + + // Create a layer from the Opener + return tarball.LayerFromOpener(opener) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.16/pkg/container/runner.go new/melange-0.23.17/pkg/container/runner.go --- old/melange-0.23.16/pkg/container/runner.go 2025-05-08 19:18:19.000000000 +0200 +++ new/melange-0.23.17/pkg/container/runner.go 2025-05-12 19:42:00.000000000 +0200 @@ -43,7 +43,7 @@ // WorkspaceTar returns an io.ReadCloser that can be used to read the status of the workspace. // The io.ReadCloser itself is a tar stream, which can be written to an io.Writer as is, // or passed to an fs.FS processor - WorkspaceTar(ctx context.Context, cfg *Config) (io.ReadCloser, error) + WorkspaceTar(ctx context.Context, cfg *Config, extraFiles []string) (io.ReadCloser, error) } type Loader interface { ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.CpuqtI/_old 2025-05-13 20:06:11.468650704 +0200 +++ /var/tmp/diff_new_pack.CpuqtI/_new 2025-05-13 20:06:11.472650872 +0200 @@ -1,5 +1,5 @@ name: melange -version: 0.23.16 -mtime: 1746724699 -commit: 5b4c1e4b78a5373233946c0d9c81442f85f45dad +version: 0.23.17 +mtime: 1747071720 +commit: 76a4bd045fcad1727125b4ee7c12e97cf145c27c ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.30101/vendor.tar.gz differ: char 134, line 3