Skip to content

Commit 56ff891

Browse files
The127claude
andauthored
Build images for the other architecture (#48)
* feat: run a machine of another architecture on TCG KVM runs only machines of the host's architecture, so the check of an image built for another one boots on TCG and says why. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: run a machine that names no architecture as amd64 Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: key layers by the architecture they are built for A scratch stage has no base digest, so without the architecture in the key an amd64 and an arm64 build would share its layers. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * refactor: take the architecture of a build from its command Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: ask for a disk of the architecture of the plan Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: make a disk with the loader and stub of its architecture The image may be of another architecture than the builder VM the agent runs on, so the request names it. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: tell a run the architecture it builds for TARGETARCH lets one Imagefile install the kernel of either architecture. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: tell a shell the architecture its build is for Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: load binfmt_misc in the builder VM Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: take the emulator of an image's architecture out of a pinned qemu-user package The builder VM runs the programs of an image of another architecture through it. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: refuse an emulator miso has no QEMU for, naming the architecture Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: carry the emulator of another architecture in the builder's initramfs Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: give every builder initramfs a directory of emulators Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: register the builder VM's emulators on binfmt_misc They are registered as Debian registers its qemu-user, through a name that keeps a program's argv[0], and opened right away, so a run in its own root has them too. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: refuse an emulator of an architecture without a binfmt_misc entry Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: write the builder's boot files with the emulators of a build Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: boot the builder with the emulator of the architecture it builds for Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: build an image for the other architecture The builder VM keeps running on the host's architecture, on KVM. The steps of the image run through QEMU in user mode and its checks boot on TCG. Closes #31 Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: plan a build for the architecture --arch names Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: open a shell on the layers of the architecture --arch names Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * feat: name the architecture in the shell a failed step hints at Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * refactor: take the architecture only from --arch Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * build: recommend the qemu-system of the other architecture Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * fix: set TARGETARCH once when the build file sets it Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * refactor: build the arch list in error messages from arches Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * refactor: read --arch once in withBuilder Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> * refactor: return the program from the loop in emulator.ready Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: karo <karolin.kostial@gmail.com> --------- Signed-off-by: karo <karolin.kostial@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0f52bd5 commit 56ff891

61 files changed

Lines changed: 925 additions & 91 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.goreleaser.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ nfpms:
4646
deb:
4747
dependencies:
4848
- qemu-system-x86
49+
recommends:
50+
- qemu-system-arm
4951
rpm:
5052
dependencies:
5153
- qemu-system-x86-core
54+
recommends:
55+
- qemu-system-aarch64-core
5256
- id: arm64
5357
ids: [arm64]
5458
package_name: miso
@@ -66,9 +70,13 @@ nfpms:
6670
deb:
6771
dependencies:
6872
- qemu-system-arm
73+
recommends:
74+
- qemu-system-x86
6975
rpm:
7076
dependencies:
7177
- qemu-system-aarch64-core
78+
recommends:
79+
- qemu-system-x86-core
7280

7381
checksum:
7482
name_template: checksums.txt

‎arch-go.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,20 @@ dependenciesRules:
452452
- os/exec
453453
- net
454454
- syscall
455+
# the emulator takes miso's pinned qemu-user package apart into what runs
456+
# the programs of an image of another architecture. It has the download
457+
# fetch the package, and never starts a process, opens a connection or
458+
# touches the kernel
459+
- package: '**.internal.emulator$'
460+
shouldOnlyDependsOn:
461+
internal:
462+
- '**.internal.deb$'
463+
- '**.internal.download$'
464+
shouldNotDependsOn:
465+
standard:
466+
- os/exec
467+
- net
468+
- syscall
455469
# the builder kernel takes miso's pinned Debian package apart into the
456470
# kernel the builder VM boots and the modules its agent loads. It has the
457471
# download fetch the package, and never starts a process, opens a

‎cmd/miso/arch_test.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package main_test
22

33
import (
4+
"context"
5+
"errors"
6+
"net/http"
47
"testing"
58

69
"github.com/stretchr/testify/assert"
710
"github.com/stretchr/testify/require"
811

912
main "github.com/The127/miso/cmd/miso"
13+
"github.com/The127/miso/internal/download"
1014
)
1115

1216
func TestAHostMisoDoesNotBuildOnIsRefusedNamingItAndWhereMisoBuilds(t *testing.T) {
@@ -18,3 +22,25 @@ func TestAHostMisoDoesNotBuildOnIsRefusedNamingItAndWhereMisoBuilds(t *testing.T
1822
assert.ErrorContains(t, err, "riscv64")
1923
assert.ErrorContains(t, err, "amd64 and arm64")
2024
}
25+
26+
func TestABuildForTheBuildersOwnArchitectureFetchesNoEmulator(t *testing.T) {
27+
// arrange
28+
offline := &http.Client{Transport: roundTrip(func(*http.Request) (*http.Response, error) {
29+
return nil, errors.New("offline")
30+
})}
31+
store := download.Open(t.TempDir(), offline)
32+
33+
// act
34+
emulators, err := main.EmulatorsFor(context.Background(), store, main.BuilderArch)
35+
36+
// assert
37+
require.NoError(t, err)
38+
assert.Empty(t, emulators)
39+
}
40+
41+
// roundTrip is an HTTP transport that is a function.
42+
type roundTrip func(*http.Request) (*http.Response, error)
43+
44+
func (f roundTrip) RoundTrip(request *http.Request) (*http.Response, error) {
45+
return f(request)
46+
}

‎cmd/miso/build.go‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"context"
5-
"fmt"
65
"io"
76
"os"
87

@@ -22,14 +21,14 @@ var buildCommand = &cli.Command{
2221
Flags: []cli.Flag{
2322
fileFlag(),
2423
outputFlag(),
25-
&cli.StringFlag{Name: "arch", Usage: "the architecture the image is built for", Value: builderArch},
24+
archFlag(),
2625
},
2726
Action: runBuild,
2827
}
2928

3029
func runBuild(ctx context.Context, command *cli.Command) error {
31-
if arch := command.String("arch"); arch != builderArch {
32-
return fmt.Errorf("--arch %q is not an architecture miso builds for, only %s", arch, builderArch)
30+
if err := archRefusal(command); err != nil {
31+
return err
3332
}
3433

3534
return withBuilder(ctx, command, build.Requests, func(have prepared, boot booter) error {
@@ -49,7 +48,7 @@ func runBuild(ctx context.Context, command *cli.Command) error {
4948

5049
defer func() { _ = os.RemoveAll(scratch) }()
5150

52-
outputs, err := outputsOf(command, checker(ctx, have.blobs, string(have.dir), console), scratch)
51+
outputs, err := outputsOf(command, checker(ctx, have.blobs, string(have.dir), console, targetArch(command)), scratch)
5352
if err != nil {
5453
return err
5554
}

‎cmd/miso/cache.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ func cacheDir() (string, error) {
2525
return filepath.Join(dir, "miso"), nil
2626
}
2727

28-
// baseImages are the base images miso keeps in its cache for the builder's
28+
// baseImages are the base images miso keeps in its cache for an
2929
// architecture, and the store their bytes live in.
30-
func baseImages(cache string) (*download.Store, *baseimage.Cache, error) {
30+
func baseImages(cache, arch string) (*download.Store, *baseimage.Cache, error) {
3131
if err := hostRefusal(builderArch); err != nil {
3232
return nil, nil, err
3333
}
3434

3535
blobs := download.Open(filepath.Join(cache, "bases"), http.DefaultClient)
3636

37-
return blobs, baseimage.OpenFor(filepath.Join(cache, "bases"), builderArch, blobs), nil
37+
return blobs, baseimage.OpenFor(filepath.Join(cache, "bases"), arch, blobs), nil
3838
}
3939

4040
// cacheDiskSize is how large the cache disk may grow. The file is sparse,

‎cmd/miso/checks.go‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ const bootPatience = 5 * time.Minute
2424

2525
// checker boots each checked output on a vsock of its own and runs its checks:
2626
// a rootfs with its kernel and initrd, any other output under UEFI. The boots
27-
// write their scratch into dir and their console to console.
28-
func checker(ctx context.Context, blobs *download.Store, dir string, console io.Writer) builder.Check {
27+
// write their scratch into dir and their console to console. The outputs are
28+
// of arch.
29+
func checker(ctx context.Context, blobs *download.Store, dir string, console io.Writer, arch string) builder.Check {
2930
return func(disk string, request build.Request) error {
3031
namespace, err := vsockns.Open()
3132
if errors.Is(err, vsockns.ErrNotPrivate) {
@@ -52,7 +53,7 @@ func checker(ctx context.Context, blobs *download.Store, dir string, console io.
5253
Dir: scratch,
5354
Console: console,
5455
Patience: bootPatience,
55-
Arch: builderArch,
56+
Arch: arch,
5657
}
5758

5859
if err := bootsFrom(ctx, blobs, disk, request, &boot); err != nil {
@@ -78,7 +79,7 @@ func checker(ctx context.Context, blobs *download.Store, dir string, console io.
7879
// a rootfs, the firmware for any other output.
7980
func bootsFrom(ctx context.Context, blobs *download.Store, disk string, request build.Request, boot *check.Boot) error {
8081
if request.Boot == nil {
81-
found, err := firmware.Ready(ctx, blobs, builderArch)
82+
found, err := firmware.Ready(ctx, blobs, boot.Arch)
8283
if err != nil {
8384
return err
8485
}

‎cmd/miso/export_test.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,18 @@ var CachedKeys = cachedKeys
55

66
// HostRefusal is hostRefusal for the tests.
77
var HostRefusal = hostRefusal
8+
9+
// EmulatorsFor is emulatorsFor for the tests.
10+
var EmulatorsFor = emulatorsFor
11+
12+
// BuilderArch is builderArch for the tests.
13+
const BuilderArch = builderArch
14+
15+
// WithShellHint is withShellHint for the tests.
16+
var WithShellHint = withShellHint
17+
18+
// FileFlag is fileFlag for the tests.
19+
var FileFlag = fileFlag
20+
21+
// ArchFlag is archFlag for the tests.
22+
var ArchFlag = archFlag

‎cmd/miso/plan.go‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package main
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"path/filepath"
8+
"slices"
9+
"strings"
710

811
"github.com/urfave/cli/v3"
912

@@ -19,17 +22,22 @@ var planCommand = &cli.Command{
1922
ArgsUsage: "[context]",
2023
Flags: []cli.Flag{
2124
fileFlag(),
25+
archFlag(),
2226
},
2327
Action: listPlan,
2428
}
2529

2630
func listPlan(ctx context.Context, command *cli.Command) error {
31+
if err := archRefusal(command); err != nil {
32+
return err
33+
}
34+
2735
cache, err := cacheDir()
2836
if err != nil {
2937
return err
3038
}
3139

32-
blobs, bases, err := baseImages(cache)
40+
blobs, bases, err := baseImages(cache, targetArch(command))
3341
if err != nil {
3442
return err
3543
}
@@ -65,14 +73,35 @@ func planOf(command *cli.Command, files *buildcontext.Dir, bases plan.Bases) (pl
6573
return plan.Plan{}, err
6674
}
6775

68-
planned, err := plan.New(stages, agentName(), builderArch, files, bases)
76+
planned, err := plan.New(stages, agentName(), targetArch(command), files, bases)
6977
if err != nil {
7078
return plan.Plan{}, imagefile.InFile(file, err)
7179
}
7280

7381
return planned, nil
7482
}
7583

84+
// archFlag names the architecture a command builds for. Each command gets
85+
// its own, since a flag keeps what was parsed into it.
86+
func archFlag() *cli.StringFlag {
87+
return &cli.StringFlag{Name: "arch", Usage: "the architecture the image is built for", Value: builderArch}
88+
}
89+
90+
// archRefusal is why miso cannot build for the architecture the command
91+
// names, or nil when it can.
92+
func archRefusal(command *cli.Command) error {
93+
if arch := command.String("arch"); !slices.Contains(arches, arch) {
94+
return fmt.Errorf("--arch %q is not an architecture miso builds for, only %s", arch, strings.Join(arches, " and "))
95+
}
96+
97+
return nil
98+
}
99+
100+
// targetArch is the architecture the command builds for.
101+
func targetArch(command *cli.Command) string {
102+
return command.String("arch")
103+
}
104+
76105
// fileFlagName is the flag that names the build file.
77106
const fileFlagName = "file"
78107

‎cmd/miso/prune.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func runPrune(ctx context.Context, command *cli.Command) error {
4141
return err
4242
}
4343

44-
blobs, _, err := baseImages(cache)
44+
blobs, _, err := baseImages(cache, builderArch)
4545
if err != nil {
4646
return err
4747
}
@@ -77,7 +77,7 @@ func pruneLayers(ctx context.Context, command *cli.Command, dir cachedisk.Dir, b
7777
// askBuilder has the builder VM answer one request that reaches nothing
7878
// outside the VM, from the cache disk in dir, which the caller holds.
7979
func askBuilder(ctx context.Context, command *cli.Command, dir cachedisk.Dir, blobs *download.Store, request protocol.Message, out io.Writer) error {
80-
boot, err := bootFiles(ctx, blobs)
80+
boot, err := bootFiles(ctx, blobs, builderArch)
8181
if err != nil {
8282
return err
8383
}

‎cmd/miso/shell.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ var shellCommand = &cli.Command{
2222
Metadata: map[string]any{takesWords: true},
2323
Flags: []cli.Flag{
2424
fileFlag(),
25+
archFlag(),
2526
&cli.IntFlag{Name: "before", Usage: "the line of the build file whose step the shell stands before, the end of the build if not given"},
2627
},
2728
Action: runShell,
2829
}
2930

3031
func runShell(ctx context.Context, command *cli.Command) error {
32+
if err := archRefusal(command); err != nil {
33+
return err
34+
}
35+
3136
shellOn := func(planned plan.Plan, network protocol.Network) ([]build.Request, error) {
3237
if !command.IsSet("before") {
3338
return build.After(planned, network)

0 commit comments

Comments
 (0)