// Package rosa provides Rosa OS toolchain artifacts and miscellaneous software. package rosa import ( "errors" "log" "runtime" "slices" "strconv" "strings" "hakurei.app/container/fhs" "hakurei.app/internal/pkg" ) const ( // kindEtc is the kind of [pkg.Artifact] of cureEtc. kindEtc = iota + pkg.KindCustomOffset // kindBusyboxBin is the kind of [pkg.Artifact] of busyboxBin. kindBusyboxBin ) // mustDecode is like [pkg.MustDecode], but replaces the zero value and prints // a warning. func mustDecode(s string) pkg.Checksum { var fallback = pkg.Checksum{} if s == "" { log.Println( "falling back to", pkg.Encode(fallback), "for unpopulated checksum", ) return fallback } return pkg.MustDecode(s) } var ( // AbsUsrSrc is the conventional directory to place source code under. AbsUsrSrc = fhs.AbsUsr.Append("src") // AbsSystem is the Rosa OS installation prefix. AbsSystem = fhs.AbsRoot.Append("system") ) // linuxArch returns the architecture name used by linux corresponding to // [runtime.GOARCH]. func linuxArch() string { switch runtime.GOARCH { case "amd64": return "x86_64" case "arm64": return "aarch64" default: panic("unsupported target " + runtime.GOARCH) } } // triplet returns the Rosa OS host triple corresponding to [runtime.GOARCH]. func triplet() string { return linuxArch() + "-rosa-linux-musl" } const ( // EnvTriplet holds the return value of triplet. EnvTriplet = "ROSA_TRIPLE" ) // earlyLDFLAGS returns LDFLAGS corresponding to triplet. func earlyLDFLAGS(static bool) string { s := "-fuse-ld=lld " + "-L/system/lib -Wl,-rpath=/system/lib " + "-L/system/lib/" + triplet() + " " + "-Wl,-rpath=/system/lib/" + triplet() + " " + "-rtlib=compiler-rt " + "-unwindlib=libunwind " + "-Wl,--as-needed" if !static { s += " -Wl,--dynamic-linker=/system/bin/linker" } return s } // earlyCFLAGS is reference CFLAGS for the stage0 toolchain. const earlyCFLAGS = "-Qunused-arguments " + "-isystem/system/include" // earlyCXXFLAGS returns reference CXXFLAGS for the stage0 toolchain // corresponding to [runtime.GOARCH]. func earlyCXXFLAGS() string { return "--start-no-unused-arguments " + "-stdlib=libc++ " + "--end-no-unused-arguments " + "-isystem/system/include/c++/v1 " + "-isystem/system/include/" + triplet() + "/c++/v1 " + "-isystem/system/include " } // Toolchain denotes the infrastructure to compile a [pkg.Artifact] on. type Toolchain uint32 const ( // _toolchainBusybox denotes a busybox installation from the busyboxBin // binary distribution. This is defined as a toolchain to make use of the // toolchain abstractions to preprocess toolchainGentoo and is not a real, // functioning toolchain. It does not contain any compilers. _toolchainBusybox Toolchain = iota // toolchainGentoo denotes the toolchain in a Gentoo stage3 tarball. Special // care must be taken to compile correctly against this toolchain. toolchainGentoo // toolchainIntermediateGentoo is like to toolchainIntermediate, but // compiled against toolchainGentoo. toolchainIntermediateGentoo // toolchainStdGentoo is like Std, but bootstrapped from toolchainGentoo. // This toolchain creates the first [Stage0] distribution. toolchainStdGentoo // toolchainStage0 denotes the stage0 toolchain. Special care must be taken // to compile correctly against this toolchain. toolchainStage0 // toolchainIntermediate denotes the intermediate toolchain compiled against // toolchainStage0. This toolchain should be functionally identical to [Std] // and is used to bootstrap [Std]. toolchainIntermediate // Std denotes the standard Rosa OS toolchain. Std // _toolchainEnd is the total number of toolchains available and does not // denote a valid toolchain. _toolchainEnd ) // isStage0 returns whether t is a stage0 toolchain. func (t Toolchain) isStage0() bool { switch t { case toolchainGentoo, toolchainStage0: return true default: return false } } // isIntermediate returns whether t is an intermediate toolchain. func (t Toolchain) isIntermediate() bool { switch t { case toolchainIntermediateGentoo, toolchainIntermediate: return true default: return false } } // isStd returns whether t is considered functionally equivalent to [Std]. func (t Toolchain) isStd() bool { switch t { case toolchainStdGentoo, Std: return true default: return false } } // stage0Concat concatenates s and values. If the current toolchain is // toolchainStage0, stage0Concat returns s as is. func stage0Concat[S ~[]E, E any](t Toolchain, s S, values ...E) S { if t.isStage0() { return s } return slices.Concat(s, values) } // stage0ExclConcat concatenates s and values. If the current toolchain is not // toolchainStage0, stage0ExclConcat returns s as is. func stage0ExclConcat[S ~[]E, E any](t Toolchain, s S, values ...E) S { if t.isStage0() { return slices.Concat(s, values) } return s } // lastIndexFunc is like [strings.LastIndexFunc] but for [slices]. func lastIndexFunc[S ~[]E, E any](s S, f func(E) bool) (i int) { if i = slices.IndexFunc(s, f); i < 0 { return } if i0 := lastIndexFunc[S](s[i+1:], f); i0 >= 0 { i = i0 } return } // fixupEnviron fixes up PATH, prepends extras and returns the resulting slice. func fixupEnviron(env, extras []string, paths ...string) []string { const pathPrefix = "PATH=" pathVal := strings.Join(paths, ":") if i := lastIndexFunc(env, func(s string) bool { return strings.HasPrefix(s, pathPrefix) }); i < 0 { env = append(env, pathPrefix+pathVal) } else { if len(env[i]) == len(pathPrefix) { env[i] = pathPrefix + pathVal } else { env[i] += ":" + pathVal } } return append(extras, env...) } // absCureScript is the absolute pathname [Toolchain.New] places the fixed-up // build script under. var absCureScript = AbsSystem.Append(".cure-script") const ( // TExclusive denotes an exclusive [pkg.Artifact]. TExclusive = 1 << iota // TEarly hints for an early variant of [Toybox] to be used when available. TEarly // TNoToolchain excludes the LLVM toolchain. TNoToolchain ) var ( // gentooStage3 is the url of a Gentoo stage3 tarball. gentooStage3 string // gentooStage3Checksum is the expected checksum of gentooStage3. gentooStage3Checksum pkg.Checksum ) // SetGentooStage3 sets the Gentoo stage3 tarball url and checksum. It panics // if given zero values or if these values have already been set. func SetGentooStage3(url string, checksum pkg.Checksum) { if gentooStage3 != "" { panic(errors.New("attempting to set Gentoo stage3 url twice")) } if url == "" { panic(errors.New("attempting to set Gentoo stage3 url to the zero value")) } gentooStage3, gentooStage3Checksum = url, checksum } // New returns a [pkg.Artifact] compiled on this toolchain. func (t Toolchain) New( name string, flag int, extra []pkg.Artifact, knownChecksum *pkg.Checksum, env []string, script string, paths ...pkg.ExecPath, ) pkg.Artifact { const lcMessages = "LC_MESSAGES=C.UTF-8" var support []pkg.Artifact switch t { case _toolchainBusybox: name += "-early" support = slices.Concat([]pkg.Artifact{newBusyboxBin()}, extra) env = fixupEnviron(env, nil, "/system/bin") case toolchainGentoo, toolchainStage0: name += "-boot" support = append(support, cureEtc{}) if t == toolchainStage0 { support = append(support, NewStage0()) } else { support = append(support, _toolchainBusybox.New("gentoo", 0, nil, nil, nil, ` tar -C /work -xf /usr/src/stage3.tar.xz rm -rf /work/dev/ /work/proc/ ln -vs ../usr/bin /work/bin mkdir -vp /work/system/bin (cd /work/system/bin && ln -vs \ ../../bin/sh \ ../../usr/lib/llvm/*/bin/* \ .) `, pkg.Path(AbsUsrSrc.Append("stage3.tar.xz"), false, pkg.NewHTTPGet( nil, gentooStage3, gentooStage3Checksum, ), ))) } support = slices.Concat(support, extra) env = fixupEnviron(env, []string{ EnvTriplet + "=" + triplet(), lcMessages, "LDFLAGS=" + earlyLDFLAGS(true), }, "/system/bin", "/usr/bin", ) case toolchainIntermediateGentoo, toolchainStdGentoo, toolchainIntermediate, Std: if t.isIntermediate() { name += "-std" } boot := t - 1 musl, compilerRT, runtimes, clang := boot.NewLLVM() toybox := Toybox if flag&TEarly != 0 { toybox = toyboxEarly } std := []pkg.Artifact{cureEtc{newIANAEtc()}, musl} toolchain := []pkg.Artifact{compilerRT, runtimes, clang} utils := []pkg.Artifact{ boot.Load(Mksh), boot.Load(toybox), } if flag&TNoToolchain != 0 { toolchain = nil } support = slices.Concat(extra, std, toolchain, utils) env = fixupEnviron(env, []string{ EnvTriplet + "=" + triplet(), lcMessages, "AR=ar", "RANLIB=ranlib", "LIBCC=/system/lib/clang/21/lib/" + triplet() + "/libclang_rt.builtins.a", }, "/system/bin", "/bin") default: panic("unsupported toolchain " + strconv.Itoa(int(t))) } return pkg.NewExec( name, knownChecksum, pkg.ExecTimeoutMax, flag&TExclusive != 0, fhs.AbsRoot, env, AbsSystem.Append("bin", "sh"), []string{"sh", absCureScript.String()}, slices.Concat([]pkg.ExecPath{pkg.Path( fhs.AbsRoot, true, support..., ), pkg.Path( absCureScript, false, pkg.NewFile(".cure-script", []byte("set -eu -o pipefail\n"+script)), )}, paths)..., ) } // NewPatchedSource returns [pkg.Artifact] of source with patches applied. If // passthrough is true, source is returned as is for zero length patches. func (t Toolchain) NewPatchedSource( name, version string, source pkg.Artifact, passthrough bool, patches ...[2]string, ) pkg.Artifact { if passthrough && len(patches) == 0 { return source } paths := make([]pkg.ExecPath, len(patches)+1) for i, p := range patches { paths[i+1] = pkg.Path( AbsUsrSrc.Append(name+"-patches", p[0]+".patch"), false, pkg.NewFile(p[0]+".patch", []byte(p[1])), ) } paths[0] = pkg.Path(AbsUsrSrc.Append(name), false, source) aname := name + "-" + version + "-src" script := ` cp -r /usr/src/` + name + `/. /work/. chmod -R +w /work && cd /work ` if len(paths) > 1 { script += ` cat /usr/src/` + name + `-patches/* | \ patch \ -p 1 \ --ignore-whitespace ` aname += "-patched" } return t.New(aname, 0, stage0Concat(t, []pkg.Artifact{}, t.Load(Patch), ), nil, nil, script, paths...) } // helperInPlace is a special directory value for omitting the cd statement. const helperInPlace = "\x00" // Helper is a build system helper for [Toolchain.NewPackage]. type Helper interface { // name returns the value passed to the name argument of [Toolchain.New]. name(name, version string) string // extra returns helper-specific dependencies. extra(flag int) []PArtifact // wantsChmod returns whether the source directory should be made writable. wantsChmod() bool // wantsWrite returns whether the source directory should be mounted writable. wantsWrite() bool // scriptEarly returns the helper-specific segment of cure script that goes // before the cd statement. scriptEarly() string // createDir returns whether the path returned by wantsDir should be created. createDir() bool // wantsDir returns the directory to enter before script. // // The zero value implies source directory if [PackageAttr.ScriptEarly] is // also empty. The special value helperInPlace omits the cd statement. wantsDir() string // script returns the helper-specific segment of cure script. script(name string) string } const ( // sourceTarXZ denotes a source tarball to be decompressed using [XZ]. sourceTarXZ = 1 + iota ) // PackageAttr holds build-system-agnostic attributes. type PackageAttr struct { // Mount the source tree writable. Writable bool // Do not pass through [Toolchain.NewPatchedSource]. Chmod bool // Unconditionally enter source directory early. EnterSource bool // Additional environment variables. Env []string // Runs before script emitted by [Helper]. Enters source if non-empty. ScriptEarly string // Passed to [Toolchain.NewPatchedSource]. Patches [][2]string // Kind of source artifact. SourceKind int // Dependencies not provided by stage0. NonStage0 []pkg.Artifact // Passed through to [Toolchain.New], before source. Paths []pkg.ExecPath // Passed through to [Toolchain.New]. Flag int } // NewPackage constructs a [pkg.Artifact] via a build system helper. func (t Toolchain) NewPackage( name, version string, source pkg.Artifact, attr *PackageAttr, helper Helper, extra ...PArtifact, ) pkg.Artifact { if attr == nil { attr = new(PackageAttr) } if name == "" || version == "" { panic("name must be non-empty") } if source == nil { panic("source must be non-nil") } wantsChmod, wantsWrite := helper.wantsChmod(), helper.wantsWrite() if attr.SourceKind > 0 && (attr.Writable || attr.Chmod || wantsChmod || wantsWrite || len(attr.Patches) > 0) { panic("source processing requested on a non-unpacked kind") } dc := len(attr.NonStage0) if !t.isStage0() { dc += 1<<3 + len(extra) } extraRes := make([]pkg.Artifact, 0, dc) extraRes = append(extraRes, attr.NonStage0...) if !t.isStage0() { for _, p := range helper.extra(attr.Flag) { extraRes = append(extraRes, t.Load(p)) } for _, p := range extra { extraRes = append(extraRes, t.Load(p)) } } var scriptEarly string var sourceSuffix string switch attr.SourceKind { case sourceTarXZ: sourceSuffix = ".tar.xz" scriptEarly += ` tar -C /usr/src/ -xf '/usr/src/` + name + `.tar.xz' mv '/usr/src/` + name + `-` + version + `' '/usr/src/` + name + `' ` break } dir := helper.wantsDir() helperScriptEarly := helper.scriptEarly() if attr.EnterSource || dir == "" || attr.ScriptEarly != "" || helperScriptEarly != "" { scriptEarly += ` cd '/usr/src/` + name + `/' ` } scriptEarly += attr.ScriptEarly + helperScriptEarly if dir != "" && dir != helperInPlace { if helper.createDir() { scriptEarly += "\nmkdir -p " + dir } scriptEarly += "\ncd " + dir + "\n" } else if !attr.EnterSource && attr.ScriptEarly == "" { panic("cannot remain in root") } return t.New( helper.name(name, version), attr.Flag, extraRes, nil, attr.Env, scriptEarly+helper.script(name), slices.Concat(attr.Paths, []pkg.ExecPath{ pkg.Path(AbsUsrSrc.Append( name+sourceSuffix, ), attr.Writable || wantsWrite, t.NewPatchedSource( name, version, source, !attr.Chmod && !wantsChmod, attr.Patches..., )), })..., ) }