Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dc7427e
feat: run a machine of another architecture on TCG
The127 Oct 8, 2026
587589d
feat: run a machine that names no architecture as amd64
The127 Oct 8, 2026
d634c88
feat: key layers by the architecture they are built for
The127 Oct 8, 2026
8a0f2a4
refactor: take the architecture of a build from its command
The127 Oct 8, 2026
489ebfd
feat: ask for a disk of the architecture of the plan
The127 Oct 9, 2026
4587a52
feat: make a disk with the loader and stub of its architecture
The127 Oct 9, 2026
aeaee9d
feat: tell a run the architecture it builds for
The127 Oct 9, 2026
211483f
feat: tell a shell the architecture its build is for
The127 Oct 9, 2026
51c7183
feat: load binfmt_misc in the builder VM
The127 Oct 10, 2026
e5ffbd6
feat: take the emulator of an image's architecture out of a pinned qe…
The127 Oct 10, 2026
42da282
feat: refuse an emulator miso has no QEMU for, naming the architecture
The127 Oct 10, 2026
1b28ed4
feat: carry the emulator of another architecture in the builder's ini…
The127 Oct 10, 2026
7d041c3
feat: give every builder initramfs a directory of emulators
The127 Oct 10, 2026
d5b3a28
feat: register the builder VM's emulators on binfmt_misc
The127 Oct 10, 2026
9e5202f
feat: refuse an emulator of an architecture without a binfmt_misc entry
The127 Oct 10, 2026
aa6e581
feat: write the builder's boot files with the emulators of a build
The127 Oct 10, 2026
4e75184
feat: boot the builder with the emulator of the architecture it build…
The127 Oct 10, 2026
dae53ee
feat: build an image for the other architecture
The127 Oct 10, 2026
936b044
feat: plan a build for the architecture --arch names
The127 Oct 10, 2026
448fadf
feat: open a shell on the layers of the architecture --arch names
The127 Oct 10, 2026
6617ebd
feat: name the architecture in the shell a failed step hints at
The127 Oct 10, 2026
d871c3b
refactor: take the architecture only from --arch
The127 Oct 10, 2026
f237b6f
build: recommend the qemu-system of the other architecture
The127 Oct 10, 2026
c00a0b1
fix: set TARGETARCH once when the build file sets it
The127 Oct 10, 2026
8c60a81
refactor: build the arch list in error messages from arches
The127 Oct 10, 2026
422642b
refactor: read --arch once in withBuilder
The127 Oct 10, 2026
6acab10
refactor: return the program from the loop in emulator.ready
The127 Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ nfpms:
deb:
dependencies:
- qemu-system-x86
recommends:
- qemu-system-arm
rpm:
dependencies:
- qemu-system-x86-core
recommends:
- qemu-system-aarch64-core
- id: arm64
ids: [arm64]
package_name: miso
Expand All @@ -66,9 +70,13 @@ nfpms:
deb:
dependencies:
- qemu-system-arm
recommends:
- qemu-system-x86
rpm:
dependencies:
- qemu-system-aarch64-core
recommends:
- qemu-system-x86-core

checksum:
name_template: checksums.txt
Expand Down
14 changes: 14 additions & 0 deletions arch-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,20 @@ dependenciesRules:
- os/exec
- net
- syscall
# the emulator takes miso's pinned qemu-user package apart into what runs
# the programs of an image of another architecture. It has the download
# fetch the package, and never starts a process, opens a connection or
# touches the kernel
- package: '**.internal.emulator$'
shouldOnlyDependsOn:
internal:
- '**.internal.deb$'
- '**.internal.download$'
shouldNotDependsOn:
standard:
- os/exec
- net
- syscall
# the builder kernel takes miso's pinned Debian package apart into the
# kernel the builder VM boots and the modules its agent loads. It has the
# download fetch the package, and never starts a process, opens a
Expand Down
26 changes: 26 additions & 0 deletions cmd/miso/arch_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package main_test

import (
"context"
"errors"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

main "github.com/The127/miso/cmd/miso"
"github.com/The127/miso/internal/download"
)

func TestAHostMisoDoesNotBuildOnIsRefusedNamingItAndWhereMisoBuilds(t *testing.T) {
Expand All @@ -18,3 +22,25 @@ func TestAHostMisoDoesNotBuildOnIsRefusedNamingItAndWhereMisoBuilds(t *testing.T
assert.ErrorContains(t, err, "riscv64")
assert.ErrorContains(t, err, "amd64 and arm64")
}

func TestABuildForTheBuildersOwnArchitectureFetchesNoEmulator(t *testing.T) {
// arrange
offline := &http.Client{Transport: roundTrip(func(*http.Request) (*http.Response, error) {
return nil, errors.New("offline")
})}
store := download.Open(t.TempDir(), offline)

// act
emulators, err := main.EmulatorsFor(context.Background(), store, main.BuilderArch)

// assert
require.NoError(t, err)
assert.Empty(t, emulators)
}

// roundTrip is an HTTP transport that is a function.
type roundTrip func(*http.Request) (*http.Response, error)

func (f roundTrip) RoundTrip(request *http.Request) (*http.Response, error) {
return f(request)
}
9 changes: 4 additions & 5 deletions cmd/miso/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"fmt"
"io"
"os"

Expand All @@ -22,14 +21,14 @@ var buildCommand = &cli.Command{
Flags: []cli.Flag{
fileFlag(),
outputFlag(),
&cli.StringFlag{Name: "arch", Usage: "the architecture the image is built for", Value: builderArch},
archFlag(),
},
Action: runBuild,
}

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

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

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

outputs, err := outputsOf(command, checker(ctx, have.blobs, string(have.dir), console), scratch)
outputs, err := outputsOf(command, checker(ctx, have.blobs, string(have.dir), console, targetArch(command)), scratch)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/miso/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ func cacheDir() (string, error) {
return filepath.Join(dir, "miso"), nil
}

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

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

return blobs, baseimage.OpenFor(filepath.Join(cache, "bases"), builderArch, blobs), nil
return blobs, baseimage.OpenFor(filepath.Join(cache, "bases"), arch, blobs), nil
}

// cacheDiskSize is how large the cache disk may grow. The file is sparse,
Expand Down
9 changes: 5 additions & 4 deletions cmd/miso/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ const bootPatience = 5 * time.Minute

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

if err := bootsFrom(ctx, blobs, disk, request, &boot); err != nil {
Expand All @@ -78,7 +79,7 @@ func checker(ctx context.Context, blobs *download.Store, dir string, console io.
// a rootfs, the firmware for any other output.
func bootsFrom(ctx context.Context, blobs *download.Store, disk string, request build.Request, boot *check.Boot) error {
if request.Boot == nil {
found, err := firmware.Ready(ctx, blobs, builderArch)
found, err := firmware.Ready(ctx, blobs, boot.Arch)
if err != nil {
return err
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/miso/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,18 @@ var CachedKeys = cachedKeys

// HostRefusal is hostRefusal for the tests.
var HostRefusal = hostRefusal

// EmulatorsFor is emulatorsFor for the tests.
var EmulatorsFor = emulatorsFor

// BuilderArch is builderArch for the tests.
const BuilderArch = builderArch

// WithShellHint is withShellHint for the tests.
var WithShellHint = withShellHint

// FileFlag is fileFlag for the tests.
var FileFlag = fileFlag

// ArchFlag is archFlag for the tests.
var ArchFlag = archFlag
33 changes: 31 additions & 2 deletions cmd/miso/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package main

import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"

"github.com/urfave/cli/v3"

Expand All @@ -19,17 +22,22 @@ var planCommand = &cli.Command{
ArgsUsage: "[context]",
Flags: []cli.Flag{
fileFlag(),
archFlag(),
},
Action: listPlan,
}

func listPlan(ctx context.Context, command *cli.Command) error {
if err := archRefusal(command); err != nil {
return err
}

cache, err := cacheDir()
if err != nil {
return err
}

blobs, bases, err := baseImages(cache)
blobs, bases, err := baseImages(cache, targetArch(command))
if err != nil {
return err
}
Expand Down Expand Up @@ -65,14 +73,35 @@ func planOf(command *cli.Command, files *buildcontext.Dir, bases plan.Bases) (pl
return plan.Plan{}, err
}

planned, err := plan.New(stages, agentName(), builderArch, files, bases)
planned, err := plan.New(stages, agentName(), targetArch(command), files, bases)
if err != nil {
return plan.Plan{}, imagefile.InFile(file, err)
}

return planned, nil
}

// archFlag names the architecture a command builds for. Each command gets
// its own, since a flag keeps what was parsed into it.
func archFlag() *cli.StringFlag {
return &cli.StringFlag{Name: "arch", Usage: "the architecture the image is built for", Value: builderArch}
}

// archRefusal is why miso cannot build for the architecture the command
// names, or nil when it can.
func archRefusal(command *cli.Command) error {
if arch := command.String("arch"); !slices.Contains(arches, arch) {
return fmt.Errorf("--arch %q is not an architecture miso builds for, only %s", arch, strings.Join(arches, " and "))
}

return nil
}

// targetArch is the architecture the command builds for.
func targetArch(command *cli.Command) string {
return command.String("arch")
}

// fileFlagName is the flag that names the build file.
const fileFlagName = "file"

Expand Down
4 changes: 2 additions & 2 deletions cmd/miso/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func runPrune(ctx context.Context, command *cli.Command) error {
return err
}

blobs, _, err := baseImages(cache)
blobs, _, err := baseImages(cache, builderArch)
if err != nil {
return err
}
Expand Down Expand Up @@ -77,7 +77,7 @@ func pruneLayers(ctx context.Context, command *cli.Command, dir cachedisk.Dir, b
// askBuilder has the builder VM answer one request that reaches nothing
// outside the VM, from the cache disk in dir, which the caller holds.
func askBuilder(ctx context.Context, command *cli.Command, dir cachedisk.Dir, blobs *download.Store, request protocol.Message, out io.Writer) error {
boot, err := bootFiles(ctx, blobs)
boot, err := bootFiles(ctx, blobs, builderArch)
if err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/miso/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ var shellCommand = &cli.Command{
Metadata: map[string]any{takesWords: true},
Flags: []cli.Flag{
fileFlag(),
archFlag(),
&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"},
},
Action: runShell,
}

func runShell(ctx context.Context, command *cli.Command) error {
if err := archRefusal(command); err != nil {
return err
}

shellOn := func(planned plan.Plan, network protocol.Network) ([]build.Request, error) {
if !command.IsSet("before") {
return build.After(planned, network)
Expand Down
4 changes: 4 additions & 0 deletions cmd/miso/shellhint.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ func withShellHint(command *cli.Command, requests []build.Request, err error) er
}

words := []string{"miso", "shell", "--before", strconv.Itoa(located.Line)}
if arch := targetArch(command); arch != builderArch {
words = append(words, "--arch", arch)
}

if file := command.String(fileFlagName); strings.HasPrefix(file, "-") {
words = append(words, "--file="+file)
} else if file != "" {
Expand Down
48 changes: 48 additions & 0 deletions cmd/miso/shellhint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main_test

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"

main "github.com/The127/miso/cmd/miso"
"github.com/The127/miso/internal/imagefile"
)

// hinted is the error of a failed step on line 2 with its hint, as a build
// run with the arguments gives it.
func hinted(t *testing.T, args ...string) error {
t.Helper()

var got error
build := &cli.Command{
Name: "build",
Flags: []cli.Flag{main.FileFlag(), main.ArchFlag()},
Action: func(_ context.Context, command *cli.Command) error {
got = main.WithShellHint(command, nil, &imagefile.Error{Line: 2, Err: errors.New("RUN false: exit code 1")})

return nil
},
}
require.NoError(t, build.Run(context.Background(), append([]string{"build"}, args...)))

return got
}

func TestTheShellAFailingStepOfABuildForTheOtherArchitectureSaysToOpenIsForThatArchitecture(t *testing.T) {
// arrange
other := "arm64"
if main.BuilderArch == "arm64" {
other = "amd64"
}

// act
err := hinted(t, "--arch", other)

// assert
assert.ErrorContains(t, err, "miso shell --before 2 --arch "+other)
}
Loading
Loading