diff --git a/machine/hyperlight/README.md b/machine/hyperlight/README.md new file mode 100644 index 000000000..2ec6eb00e --- /dev/null +++ b/machine/hyperlight/README.md @@ -0,0 +1,52 @@ +## Hyperlight Machine Driver + +A kraftkit machine driver that runs Unikraft unikernels on +[Hyperlight](https://github.com/hyperlight-dev/hyperlight) micro-VMs. Each +machine created through this driver is a detached `hyperlight-unikraft` child +process, so `kraft ps`, `kraft stop`, `kraft rm`, and `kraft logs` all work +across separate kraft invocations via standard PID tracking. + +### Requirements + +- Linux host with `/dev/kvm` read/write access, or Windows host with the + Windows Hypervisor Platform (WHP) enabled. +- The `hyperlight-unikraft` binary (from + [danbugs/hyperlight-unikraft](https://github.com/danbugs/hyperlight-unikraft)) + installed on `$PATH`: + + ```bash + cargo install --git https://github.com/danbugs/hyperlight-unikraft \ + --branch main hyperlight-unikraft-host --bin hyperlight-unikraft + ``` + +- Unikraft kernels built for the `hyperlight` platform target. + +No cgo, no linker flags, no shared libraries — kraftkit just needs to find +`hyperlight-unikraft` on `$PATH` at runtime. A future iteration may link the +Hyperlight host library in-process via cgo for tighter lifecycle control and +lower per-call overhead; the subprocess model is a deliberate first step. + +### Platform selection + +The driver registers two names: + +- `hyperlight` (canonical) +- `hl` (shorthand) + +Either can be used with `--plat`: + +```bash +kraft build --plat hyperlight --arch x86_64 +kraft run --plat hl ... +``` + +### Defaults + +- `DefaultMemory`: `16Mi`. Override with `--memory` for heavier guests. +- `DefaultStack`: `8Mi`. Not yet exposed through the CLI or Kraftfile. + +### Limitations + +- `kraft pause` is not supported; Hyperlight has no pause semantics. +- The child process is terminated on `kraft stop` via SIGTERM; there is no + in-VM quiesce step. diff --git a/machine/hyperlight/config.go b/machine/hyperlight/config.go new file mode 100644 index 000000000..c30692658 --- /dev/null +++ b/machine/hyperlight/config.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. +package hyperlight + +// HyperlightConfig represents configuration for a Hyperlight micro-VM. +type HyperlightConfig struct { + // KernelPath is the path to the unikernel binary. + KernelPath string `json:"kernelPath,omitempty"` + + // Memory is the amount of memory allocated to the VM (e.g. "512Mi"). + Memory string `json:"memory,omitempty"` + + // Stack is the stack size (e.g. "8Mi"). + Stack string `json:"stack,omitempty"` + + // InitRd is the path to the initramfs/rootfs CPIO archive. + InitRd string `json:"initrd,omitempty"` + + // LogPath is the path to the log file. + LogPath string `json:"logPath,omitempty"` +} + +type HyperlightOption func(*HyperlightConfig) error + +func NewHyperlightConfig(opts ...HyperlightOption) (*HyperlightConfig, error) { + cfg := &HyperlightConfig{ + Memory: "16Mi", + Stack: "8Mi", + } + + for _, o := range opts { + if err := o(cfg); err != nil { + return nil, err + } + } + + return cfg, nil +} + +func WithKernel(kernel string) HyperlightOption { + return func(c *HyperlightConfig) error { + c.KernelPath = kernel + return nil + } +} + +func WithMemory(memory string) HyperlightOption { + return func(c *HyperlightConfig) error { + c.Memory = memory + return nil + } +} + +func WithInitRd(initrd string) HyperlightOption { + return func(c *HyperlightConfig) error { + c.InitRd = initrd + return nil + } +} + +func WithStack(stack string) HyperlightOption { + return func(c *HyperlightConfig) error { + c.Stack = stack + return nil + } +} diff --git a/machine/hyperlight/init.go b/machine/hyperlight/init.go new file mode 100644 index 000000000..c6ef08fdf --- /dev/null +++ b/machine/hyperlight/init.go @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. +package hyperlight + +import "encoding/gob" + +func init() { + gob.Register(HyperlightConfig{}) +} diff --git a/machine/hyperlight/options.go b/machine/hyperlight/options.go new file mode 100644 index 000000000..b18097a47 --- /dev/null +++ b/machine/hyperlight/options.go @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package hyperlight + +// MachineServiceV1alpha1Option is a functional option for configuring the +// Hyperlight machine service. +type MachineServiceV1alpha1Option func(*machineV1alpha1Service) error diff --git a/machine/hyperlight/v1alpha1.go b/machine/hyperlight/v1alpha1.go new file mode 100644 index 000000000..6456832bf --- /dev/null +++ b/machine/hyperlight/v1alpha1.go @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package hyperlight + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + zip "api.zip" + "github.com/go-viper/mapstructure/v2" + goprocess "github.com/shirou/gopsutil/v3/process" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + machinev1alpha1 "kraftkit.sh/api/machine/v1alpha1" + "kraftkit.sh/config" + kraftexec "kraftkit.sh/exec" + "kraftkit.sh/internal/logtail" + "kraftkit.sh/log" + "kraftkit.sh/machine/name" +) + +const ( + // DefaultMemory is the default memory allocation for Hyperlight VMs when + // --memory is not supplied. Small guests fit comfortably; heavier + // interpreters need an explicit override upward. + DefaultMemory = "16Mi" + + // DefaultStack is the default guest stack size. Not yet surfaced through + // the kraft CLI or Kraftfile; adjust via the WithStack functional option. + // TODO: wire through Kraftfile platform config. + DefaultStack = "8Mi" + + // HostBinary is the external command that runs a Unikraft unikernel on + // Hyperlight. It must be installed in $PATH for this driver to work. + HostBinary = "hyperlight-unikraft" +) + +// machineV1alpha1Service drives Hyperlight unikernels via the hyperlight-unikraft +// binary. Each machine corresponds to a child process that owns the in-process +// Hyperlight sandbox; PID tracking mirrors the firecracker driver so kraft ps, +// stop, rm, logs, etc. work across kraft invocations. +type machineV1alpha1Service struct{} + +// NewMachineV1alpha1Service creates a new Hyperlight machine service. +func NewMachineV1alpha1Service(ctx context.Context, opts ...any) (machinev1alpha1.MachineService, error) { + service := machineV1alpha1Service{} + + for _, opt := range opts { + hopt, ok := opt.(MachineServiceV1alpha1Option) + if !ok { + continue + } + if err := hopt(&service); err != nil { + return nil, err + } + } + + return &service, nil +} + +// Create implements kraftkit.sh/api/machine/v1alpha1.MachineService.Create. +// It does not spawn the hyperlight-unikraft subprocess yet — that happens in +// Start, matching the firecracker/qemu lifecycle. +func (service *machineV1alpha1Service) Create(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + if machine.Status.KernelPath == "" { + return machine, fmt.Errorf("cannot create hyperlight instance without kernel") + } + + if machine.Spec.Emulation { + return machine, fmt.Errorf("hyperlight does not support emulation mode") + } + + if _, err := exec.LookPath(HostBinary); err != nil { + return machine, fmt.Errorf("%s not found in $PATH: %w", HostBinary, err) + } + + if machine.ObjectMeta.UID == "" { + machineID, err := name.NewRandomMachineID() + if err != nil { + return nil, fmt.Errorf("could not generate machine UID: %w", err) + } + machine.ObjectMeta.UID = types.UID(machineID.ShortString()) + } + + if machine.Status.StateDir == "" { + machine.Status.StateDir = filepath.Join( + config.G[config.KraftKit](ctx).RuntimeDir, + string(machine.ObjectMeta.UID), + ) + } + + if err := os.MkdirAll(machine.Status.StateDir, 0o755); err != nil { + return machine, fmt.Errorf("could not create state directory: %w", err) + } + + if machine.Spec.Resources.Requests.Memory().Value() == 0 { + q, err := resource.ParseQuantity(DefaultMemory) + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, err + } + machine.Spec.Resources.Requests[corev1.ResourceMemory] = q + } + + if machine.Spec.Resources.Requests.Cpu().Value() == 0 { + q, err := resource.ParseQuantity("1") + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, err + } + machine.Spec.Resources.Requests[corev1.ResourceCPU] = q + } + + logFile := filepath.Join(machine.Status.StateDir, "vmm.log") + machine.Status.LogFile = logFile + // Create the log file up front so `kraft logs --follow` can tail it the + // moment Start returns, even before the child process has written output. + if f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY, 0o644); err == nil { + f.Close() + } + + machine.Status.PlatformConfig = HyperlightConfig{ + KernelPath: machine.Status.KernelPath, + Memory: machine.Spec.Resources.Requests.Memory().String(), + Stack: DefaultStack, + InitRd: machine.Status.InitrdPath, + LogPath: logFile, + } + + machine.CreationTimestamp = metav1.Now() + machine.Status.State = machinev1alpha1.MachineStateCreated + + log.G(ctx). + WithField("uid", machine.ObjectMeta.UID). + WithField("kernel", machine.Status.KernelPath). + Debug("hyperlight instance created") + + return machine, nil +} + +// Update implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Update(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + return machine, nil +} + +// Watch implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Watch(ctx context.Context, machine *machinev1alpha1.Machine) (chan *machinev1alpha1.Machine, chan error, error) { + events := make(chan *machinev1alpha1.Machine) + errs := make(chan error) + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + updated, err := service.Get(ctx, machine) + if err != nil { + errs <- err + return + } + + if updated.Status.State != machine.Status.State { + events <- updated + machine = updated + } + + if updated.Status.State == machinev1alpha1.MachineStateExited || + updated.Status.State == machinev1alpha1.MachineStateFailed { + return + } + + time.Sleep(100 * time.Millisecond) + } + } + }() + + return events, errs, nil +} + +// Start implements kraftkit.sh/api/machine/v1alpha1.MachineService. It spawns +// the hyperlight-unikraft subprocess, redirects its stdout/stderr to the log +// file, and records the child PID in machine.Status. The process is detached +// (own process group) so the VMM continues running after kraft exits, matching +// the firecracker driver's lifecycle. +func (service *machineV1alpha1Service) Start(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + hlcfg, err := getHyperlightConfigFromPlatformConfig(machine.Status.PlatformConfig) + if err != nil { + return machine, err + } + + args := []string{ + "--memory", hlcfg.Memory, + "--stack", hlcfg.Stack, + } + if hlcfg.InitRd != "" { + args = append(args, "--initrd", hlcfg.InitRd) + } + args = append(args, hlcfg.KernelPath) + if len(machine.Spec.ApplicationArgs) > 0 { + args = append(args, "--") + args = append(args, machine.Spec.ApplicationArgs...) + } + + logFile, err := os.OpenFile(hlcfg.LogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, fmt.Errorf("could not open log file: %w", err) + } + // We duplicate this fd into the child via stdout; safe to close our + // own copy once the child is running. + defer logFile.Close() + + // Use kraftkit's exec wrapper with WithDetach so the child gets its + // own process group and survives kraft exiting — same pattern the + // firecracker driver uses. + proc, err := kraftexec.NewProcess(HostBinary, args, + kraftexec.WithStdout(logFile), + kraftexec.WithDetach(true), + ) + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, fmt.Errorf("could not prepare %s process: %w", HostBinary, err) + } + + if err := proc.Start(ctx); err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, fmt.Errorf("could not spawn %s: %w", HostBinary, err) + } + + pid, err := proc.Pid() + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateFailed + return machine, fmt.Errorf("could not read pid of spawned %s: %w", HostBinary, err) + } + + // Release the Go-side handle so the child doesn't need a Wait() + // and isn't kept alive through kraft's address space. Matches the + // "fire and forget PID-tracked lifecycle" the firecracker driver + // relies on. + if err := proc.Release(); err != nil { + log.G(ctx).WithError(err).Debug("releasing hyperlight process handle") + } + + machine.Status.Pid = int32(pid) + machine.Status.State = machinev1alpha1.MachineStateRunning + machine.Status.StartedAt = time.Now() + + log.G(ctx). + WithField("uid", machine.ObjectMeta.UID). + WithField("pid", pid). + WithField("cmd", proc.Cmdline()). + Debug("hyperlight instance started") + + return machine, nil +} + +// Pause implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Pause(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + return machine, fmt.Errorf("hyperlight does not support pause") +} + +// Stop implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Stop(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + if machine.Status.State == machinev1alpha1.MachineStateExited { + return machine, nil + } + + if machine.Status.Pid <= 0 { + machine.Status.State = machinev1alpha1.MachineStateExited + return machine, nil + } + + proc, err := goprocess.NewProcess(machine.Status.Pid) + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateExited + return machine, nil + } + + if err := proc.Terminate(); err != nil { + return machine, fmt.Errorf("could not terminate hyperlight process %d: %w", machine.Status.Pid, err) + } + + machine.Status.State = machinev1alpha1.MachineStateExited + machine.Status.ExitedAt = time.Now() + return machine, nil +} + +// Delete implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Delete(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + machine, _ = service.Stop(ctx, machine) + + if machine.Status.StateDir != "" { + os.RemoveAll(machine.Status.StateDir) + } + + return machine, nil +} + +// Get implements kraftkit.sh/api/machine/v1alpha1.MachineService.Get. +func (service *machineV1alpha1Service) Get(ctx context.Context, machine *machinev1alpha1.Machine) (*machinev1alpha1.Machine, error) { + hlcfg, err := getHyperlightConfigFromPlatformConfig(machine.Status.PlatformConfig) + if err != nil { + return machine, err + } + + if hlcfg.Memory != "" { + q, err := resource.ParseQuantity(hlcfg.Memory) + if err != nil { + return machine, fmt.Errorf("invalid memory quantity %q in platform config: %w", hlcfg.Memory, err) + } + machine.Spec.Resources.Requests[corev1.ResourceMemory] = q + } + cpu, err := resource.ParseQuantity("1") + if err != nil { + return machine, err + } + machine.Spec.Resources.Requests[corev1.ResourceCPU] = cpu + + machine.Status.PlatformConfig = *hlcfg + + // If the state is already terminal, don't second-guess it. + switch machine.Status.State { + case machinev1alpha1.MachineStateExited, + machinev1alpha1.MachineStateFailed, + machinev1alpha1.MachineStateErrored: + return machine, nil + } + + if machine.Status.Pid <= 0 { + // Not yet started. + return machine, nil + } + + proc, err := goprocess.NewProcess(machine.Status.Pid) + if err != nil { + machine.Status.State = machinev1alpha1.MachineStateExited + if machine.Status.ExitedAt.IsZero() { + machine.Status.ExitedAt = time.Now() + } + return machine, nil + } + + running, err := proc.IsRunning() + if err != nil || !running { + machine.Status.State = machinev1alpha1.MachineStateExited + if machine.Status.ExitedAt.IsZero() { + machine.Status.ExitedAt = time.Now() + } + return machine, nil + } + + machine.Status.State = machinev1alpha1.MachineStateRunning + return machine, nil +} + +// List implements kraftkit.sh/api/machine/v1alpha1.MachineService.List. +func (service *machineV1alpha1Service) List(ctx context.Context, machines *machinev1alpha1.MachineList) (*machinev1alpha1.MachineList, error) { + cached := machines.Items + machines.Items = make([]zip.Object[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus], 0, len(cached)) + + for _, machine := range cached { + updated, err := service.Get(ctx, &machine) + if err != nil { + machines.Items = cached + return machines, err + } + machines.Items = append(machines.Items, *updated) + } + + return machines, nil +} + +// Logs implements kraftkit.sh/api/machine/v1alpha1.MachineService. +func (service *machineV1alpha1Service) Logs(ctx context.Context, machine *machinev1alpha1.Machine) (chan string, chan error, error) { + hlcfg, err := getHyperlightConfigFromPlatformConfig(machine.Status.PlatformConfig) + if err != nil { + return nil, nil, err + } + return logtail.NewLogTail(ctx, hlcfg.LogPath) +} + +func getHyperlightConfigFromPlatformConfig(platformConfig interface{}) (*HyperlightConfig, error) { + if p, ok := platformConfig.(*HyperlightConfig); ok { + return p, nil + } + if p, ok := platformConfig.(HyperlightConfig); ok { + return &p, nil + } + + var hlcfg HyperlightConfig + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{Result: &hlcfg}) + if err != nil { + return nil, err + } + if err := decoder.Decode(platformConfig); err != nil { + return nil, err + } + return &hlcfg, nil +} diff --git a/machine/platform/platform.go b/machine/platform/platform.go index 35a85a820..413711ea3 100644 --- a/machine/platform/platform.go +++ b/machine/platform/platform.go @@ -12,6 +12,7 @@ const ( PlatformQEMU = Platform("qemu") PlatformKVM = PlatformQEMU PlatformXen = Platform("xen") + PlatformHyperlight = Platform("hyperlight") ) // String implements fmt.Stringer @@ -37,6 +38,8 @@ func PlatformsByName() map[string]Platform { "kvm": PlatformQEMU, "qemu": PlatformQEMU, "xen": PlatformXen, + "hyperlight": PlatformHyperlight, + "hl": PlatformHyperlight, } } @@ -46,6 +49,7 @@ func Platforms() []Platform { PlatformFirecracker, PlatformQEMU, PlatformXen, + PlatformHyperlight, } } diff --git a/machine/platform/register_linux.go b/machine/platform/register_linux.go index d86f1e387..ea724b284 100644 --- a/machine/platform/register_linux.go +++ b/machine/platform/register_linux.go @@ -13,6 +13,7 @@ import ( "kraftkit.sh/config" "kraftkit.sh/internal/set" "kraftkit.sh/machine/firecracker" + "kraftkit.sh/machine/hyperlight" "kraftkit.sh/machine/xen" "kraftkit.sh/store" ) @@ -68,6 +69,30 @@ var xenV1alpha1Driver = func(ctx context.Context, opts ...any) (machinev1alpha1. ) } +var hyperlightV1alpha1Driver = func(ctx context.Context, opts ...any) (machinev1alpha1.MachineService, error) { + service, err := hyperlight.NewMachineV1alpha1Service(ctx, opts...) + if err != nil { + return nil, err + } + + embeddedStore, err := store.NewEmbeddedStore[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus]( + filepath.Join( + config.G[config.KraftKit](ctx).RuntimeDir, + "machinev1alpha1", + ), + ) + if err != nil { + return nil, err + } + + return machinev1alpha1.NewMachineServiceHandler( + ctx, + service, + zip.WithStore[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus](embeddedStore, zip.StoreRehydrationSpecNil), + zip.WithBefore(storePlatformFilter(PlatformHyperlight)), + ) +} + func unixVariantStrategies() map[Platform]*Strategy { // TODO(jake-ciolek): The firecracker driver has a dependency on github.com/containernetworking/plugins/pkg/ns via // github.com/firecracker-microvm/firecracker-go-sdk @@ -76,6 +101,9 @@ func unixVariantStrategies() map[Platform]*Strategy { PlatformFirecracker: { NewMachineV1alpha1: firecrackerV1alpha1Driver, }, + PlatformHyperlight: { + NewMachineV1alpha1: hyperlightV1alpha1Driver, + }, } // have to check now otherwise it will error out on any interator diff --git a/machine/platform/register_windows.go b/machine/platform/register_windows.go index 57ed988ee..7a2edb75e 100644 --- a/machine/platform/register_windows.go +++ b/machine/platform/register_windows.go @@ -4,11 +4,49 @@ // You may not use this file except in compliance with the License. package platform +import ( + "context" + "path/filepath" + + zip "api.zip" + machinev1alpha1 "kraftkit.sh/api/machine/v1alpha1" + "kraftkit.sh/config" + "kraftkit.sh/machine/hyperlight" + "kraftkit.sh/store" +) + +var hyperlightV1alpha1DriverWindows = func(ctx context.Context, opts ...any) (machinev1alpha1.MachineService, error) { + service, err := hyperlight.NewMachineV1alpha1Service(ctx, opts...) + if err != nil { + return nil, err + } + + embeddedStore, err := store.NewEmbeddedStore[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus]( + filepath.Join( + config.G[config.KraftKit](ctx).RuntimeDir, + "machinev1alpha1", + ), + ) + if err != nil { + return nil, err + } + + return machinev1alpha1.NewMachineServiceHandler( + ctx, + service, + zip.WithStore[machinev1alpha1.MachineSpec, machinev1alpha1.MachineStatus](embeddedStore, zip.StoreRehydrationSpecNil), + zip.WithBefore(storePlatformFilter(PlatformHyperlight)), + ) +} + // hostSupportedStrategies returns the map of known supported drivers for the -// given host. -// No drivers are supported on Windows currently. Future HyperV support is possible. +// given host. On Windows, only Hyperlight is supported (via the +// hyperlight-unikraft subprocess, which uses Windows Hypervisor Platform or +// Hyper-V beneath it). func hostSupportedStrategies() map[Platform]*Strategy { - s := map[Platform]*Strategy{} - - return s + return map[Platform]*Strategy{ + PlatformHyperlight: { + NewMachineV1alpha1: hyperlightV1alpha1DriverWindows, + }, + } } diff --git a/unikraft/app/application.go b/unikraft/app/application.go index 69bcc448a..350682312 100644 --- a/unikraft/app/application.go +++ b/unikraft/app/application.go @@ -511,6 +511,24 @@ func (app *application) MakeArgs(ctx context.Context, tc target.Target) (*core.M orderedLibraries := []string{} for _, library := range unformattedLibraries { + if !library.IsUnpacked() { + // If the library name starts with "app-", it may have been pulled as an + // application (e.g., app-elfloader is pulled to .unikraft/apps/elfloader). + // Try finding it there instead. + if strings.HasPrefix(library.Name(), "app-") { + altPath, err := unikraft.PlaceComponent( + app.workingDir, + unikraft.ComponentTypeApp, + strings.TrimPrefix(library.Name(), "app-"), + ) + if err == nil { + if f, err := os.Stat(altPath); err == nil && f.IsDir() { + library.SetPath(altPath) + } + } + } + } + if !library.IsUnpacked() { return nil, fmt.Errorf("cannot determine library \"%s\" path without component source", library.Name()) } diff --git a/unikraft/lib/library.go b/unikraft/lib/library.go index 05d19dc1f..a304476e2 100644 --- a/unikraft/lib/library.go +++ b/unikraft/lib/library.go @@ -200,6 +200,10 @@ func (lc LibraryConfig) Path() string { return lc.path } +func (lc *LibraryConfig) SetPath(path string) { + lc.path = path +} + func (lc LibraryConfig) Compiler() string { return lc.compiler } diff --git a/unikraft/lib/transform.go b/unikraft/lib/transform.go index befdc11ed..7b37f270b 100644 --- a/unikraft/lib/transform.go +++ b/unikraft/lib/transform.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "os" + "strings" "kraftkit.sh/kconfig" "kraftkit.sh/unikraft" @@ -58,6 +59,26 @@ func TransformFromSchema(ctx context.Context, name string, props interface{}) (L } } + // If the library path doesn't exist but the library name starts with "app-", + // check if it was pulled as an application (e.g., app-elfloader is pulled to + // .unikraft/apps/elfloader). This handles the case where a component that is + // structurally a library is hosted in an "app-*" repository. + if uk != nil && uk.UK_BASE != "" { + if _, err := os.Stat(lib.path); os.IsNotExist(err) { + if strings.HasPrefix(lib.name, "app-") { + // Try finding it in apps directory without the "app-" prefix + altPath, _ := unikraft.PlaceComponent( + uk.UK_BASE, + unikraft.ComponentTypeApp, + strings.TrimPrefix(lib.name, "app-"), + ) + if f, err := os.Stat(altPath); err == nil && f.IsDir() { + lib.path = altPath + } + } + } + } + if version, ok := c["version"]; ok { lib.version, ok = version.(string) if !ok {