package main import ( "bufio" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" ) const version = "0.4.0-boredos" var ( home = os.Getenv("HOME") bin = filepath.Join(home, ".local", "bin") src = ".mol/src" db = ".mol/repo" stage = ".mol/stage" manifestDir = ".mol/manifests" reposFile = ".mol/repos" depCache = ".mol/depcache.json" defaultRepo = envOr("MOL_REPO", "https://git.sr.ht/~chersbobers/mol-pkgrepo/blob/master/PACKAGES?raw=true") goos = runtime.GOOS ) func init() { if goos == "windows" { bin = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "mol", "bin") } } func envOr(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } const ( reset = "\033[0m" bold = "\033[1m" dim = "\033[2m" green = "\033[32m" yellow = "\033[33m" cyan = "\033[36m" red = "\033[31m" ) func uiInfo(msg string) { fmt.Printf("%s →%s %s\n", cyan, reset, msg) } func uiOk(msg string) { fmt.Printf("%s ✓%s %s%s%s\n", green, reset, bold, msg, reset) } func uiWarn(msg string) { fmt.Printf("%s ⚠%s %s\n", yellow, reset, msg) } func uiErr(msg string) { fmt.Fprintf(os.Stderr, "%s ✗%s %s%s%s\n", red, reset, bold, msg, reset) } func uiDim(msg string) { fmt.Printf("%s %s%s\n", dim, msg, reset) } func uiSection(msg string) { fmt.Printf("\n%s%s▸ %s%s\n", bold, cyan, msg, reset) } func uiHeader() { fmt.Printf("%s%smol%s %sv%s · GPLv3%s\n", bold, cyan, reset, dim, version, reset) fmt.Printf("%s%s%s\n", dim, strings.Repeat("─", 42), reset) } func uiProgress(label string, total int, fn func(int)) { for i := 0; i < total; i++ { pct := ((i + 1) * 100) / total done := (30 * pct) / 100 bar := strings.Repeat("█", done) + strings.Repeat("░", 30-done) fmt.Printf("\r %s%s%s %d%% %s%s%s ", cyan, bar, reset, pct, dim, label, reset) fn(i) } fmt.Println() } func run(cmd string, args []string, dir string) bool { c := exec.Command(cmd, args...) c.Dir = dir c.Stdout = os.Stdout c.Stderr = os.Stderr return c.Run() == nil } func stageBegin(name string) string { path := filepath.Join(stage, name) os.RemoveAll(path) os.MkdirAll(path, 0755) return path } func stageCommit(name, stagedPath string) { manifestPath := filepath.Join(manifestDir, name+".list") os.MkdirAll(manifestDir, 0755) var written []string filepath.Walk(stagedPath, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } rel, _ := filepath.Rel(stagedPath, path) dest := filepath.Join("/", rel) os.MkdirAll(filepath.Dir(dest), 0755) copyFile(path, dest) written = append(written, dest) return nil }) os.WriteFile(manifestPath, []byte(strings.Join(written, "\n")), 0644) os.RemoveAll(stagedPath) uiOk(fmt.Sprintf("committed %d file(s) for %s", len(written), name)) } func stageRollback(name string) { manifestPath := filepath.Join(manifestDir, name+".list") if _, err := os.Stat(manifestPath); os.IsNotExist(err) { return } uiWarn("rolling back " + name + "…") f, _ := os.Open(manifestPath) scanner := bufio.NewScanner(f) for scanner.Scan() { path := strings.TrimSpace(scanner.Text()) if path == "" { continue } os.Remove(path) uiDim("removed " + path) } f.Close() os.Remove(manifestPath) uiOk("rollback complete for " + name) } func hasManifest(name string) bool { _, err := os.Stat(filepath.Join(manifestDir, name+".list")) return err == nil } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err } type graph map[string][]string func loadGraph() graph { g := make(graph) data, err := os.ReadFile(depCache) if err != nil { return g } json.Unmarshal(data, &g) return g } func saveGraph(g graph) { os.MkdirAll(filepath.Dir(depCache), 0755) data, _ := json.Marshal(g) os.WriteFile(depCache, data, 0644) } func resolveGraph(root string, g graph) []string { var order []string visited := make(map[string]bool) var visit func(string) visit = func(name string) { if visited[name] { return } visited[name] = true for _, dep := range g[name] { visit(dep) } order = append(order, name) } visit(root) return order } func loadRepos() []string { repos := []string{defaultRepo} f, err := os.Open(reposFile) if err != nil { return repos } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) if l == "" || strings.HasPrefix(l, "#") { continue } found := false for _, r := range repos { if r == l { found = true break } } if !found { repos = append(repos, l) } } return repos } func addRepo(url string) { os.MkdirAll(filepath.Dir(reposFile), 0755) f, _ := os.OpenFile(reposFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) defer f.Close() fmt.Fprintln(f, url) uiOk("added repo: " + url) } func lookup(t string) (string, string) { if strings.Contains(t, "://") { return t, "" } f, err := os.Open(db) if err != nil { fmt.Fprintf(os.Stderr, "%s not found — run: mol sync\n", t) os.Exit(1) } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) if l == "" || strings.HasPrefix(l, "#") { continue } parts := strings.Fields(l) if len(parts) < 2 || parts[0] != t { continue } binURL := "" for _, p := range parts { if strings.HasPrefix(p, "B=") { binURL = p[2:] } } return parts[1], binURL } fmt.Fprintf(os.Stderr, "%s not found — run: mol sync\n", t) os.Exit(1) return "", "" } func installed(name string) bool { if hasManifest(name) { return true } if _, err := os.Stat(filepath.Join(src, name)); err == nil { return true } if _, err := os.Stat(filepath.Join(bin, name)); err == nil { return true } return false } func linkBins(path string) { os.MkdirAll(bin, 0755) candidates := []string{ filepath.Join(path, "bin"), filepath.Join(path, "target", "release"), path, } for _, d := range candidates { if _, err := os.Stat(d); os.IsNotExist(err) { continue } entries, _ := os.ReadDir(d) for _, e := range entries { if e.IsDir() || strings.Contains(e.Name(), ".") { continue } full := filepath.Join(d, e.Name()) info, _ := e.Info() if info.Mode()&0111 == 0 { continue } dest := filepath.Join(bin, e.Name()) copyFile(full, dest) os.Chmod(dest, 0755) uiInfo("linked → " + dest) } } } func parseMolpkg(path string) ([]string, []string) { var deps, build []string file := filepath.Join(path, "mol.pkg") f, err := os.Open(file) if err != nil { return deps, build } defer f.Close() inBuild, inOs, inInstall := false, false, false scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) switch { case strings.Contains(l, "(deps"): for _, m := range extractQuoted(l) { deps = append(deps, m) } case strings.Contains(l, "(build"): inBuild = true case strings.Contains(l, "(install"): inBuild, inInstall = false, true case inBuild && strings.Contains(l, "("+goos): inOs = true case inBuild && inOs && strings.HasPrefix(l, "(run"): parts := extractQuoted(l) if len(parts) > 0 { build = append(build, strings.Join(parts, " ")) } case l == ")" && inOs: inOs = false case l == ")" && inBuild: inBuild = false case inInstall && strings.Contains(l, "(bin"): quoted := extractQuoted(l) if len(quoted) == 0 { continue } name := quoted[0] srcPath := filepath.Join(path, name) if goos == "windows" { if _, err := os.Stat(srcPath); os.IsNotExist(err) { srcPath = srcPath + ".exe" } } if _, err := os.Stat(srcPath); err == nil { dest := filepath.Join(bin, filepath.Base(srcPath)) copyFile(srcPath, dest) os.Chmod(dest, 0755) uiInfo("linked → " + dest) } } } return deps, build } func extractQuoted(s string) []string { var results []string for { start := strings.Index(s, `"`) if start == -1 { break } end := strings.Index(s[start+1:], `"`) if end == -1 { break } results = append(results, s[start+1:start+1+end]) s = s[start+1+end+1:] } return results } func install(name string, fromSrc, fast bool) { if installed(name) { return } uiSection("installing " + name) url, binURL := lookup(name) if (fast || !fromSrc) && binURL != "" { uiInfo("fetching prebuilt binary…") resp, err := http.Get(binURL) if err != nil || resp.StatusCode != 200 { fmt.Fprintln(os.Stderr, "download failed") os.Exit(1) } defer resp.Body.Close() os.MkdirAll(bin, 0755) dest := filepath.Join(bin, filepath.Base(binURL)) data, _ := io.ReadAll(resp.Body) os.WriteFile(dest, data, 0755) uiOk(name + " installed (binary)") return } n := strings.TrimSuffix(strings.TrimSuffix(filepath.Base(url), ".git"), ".hg") clonePath := filepath.Join(src, n) vcs := "git" if strings.Contains(url, "hg") { vcs = "hg" } uiInfo("cloning " + url + "…") if !run(vcs, []string{"clone", url, clonePath}, "") { fmt.Fprintln(os.Stderr, "clone failed") os.Exit(1) } deps, buildCmds := parseMolpkg(clonePath) g := loadGraph() g[name] = deps saveGraph(g) order := resolveGraph(name, g) for _, dep := range order { if dep != name && !installed(dep) { uiInfo("dependency: " + dep) install(dep, fromSrc, fast) } } if len(buildCmds) == 0 { fmt.Fprintf(os.Stderr, "no build steps for %s in %s\n", goos, n) os.Exit(1) } staged := stageBegin(name) uiSection("building " + name) failed := false uiProgress("compiling", len(buildCmds), func(i int) { parts := strings.Fields(buildCmds[i]) if !run(parts[0], parts[1:], clonePath) { failed = true } }) if failed { stageRollback(name) fmt.Fprintln(os.Stderr, "build failed") os.Exit(1) } linkBins(clonePath) stageCommit(name, staged) uiOk(name + " installed from source") } func removePkg(name string) { uiSection("removing " + name) if hasManifest(name) { stageRollback(name) } os.RemoveAll(filepath.Join(src, name)) os.Remove(filepath.Join(bin, name)) os.Remove(filepath.Join(bin, name+".exe")) uiOk(name + " removed") } func updatePkg(name string, fast bool) { clonePath := filepath.Join(src, name) if fast { uiInfo("fast update: reinstalling binary for " + name + "…") removePkg(name) install(name, false, true) return } uiSection("updating " + name) if _, err := os.Stat(clonePath); os.IsNotExist(err) { fmt.Fprintln(os.Stderr, name+" not found, install first") os.Exit(1) } vcs := "git" if _, err := os.Stat(filepath.Join(clonePath, ".hg")); err == nil { vcs = "hg" } args := []string{"pull"} if vcs == "hg" { args = []string{"pull", "-u"} } if !run(vcs, args, clonePath) { fmt.Fprintln(os.Stderr, "pull failed") os.Exit(1) } _, buildCmds := parseMolpkg(clonePath) staged := stageBegin(name) for _, cmd := range buildCmds { parts := strings.Fields(cmd) if !run(parts[0], parts[1:], clonePath) { stageRollback(name) fmt.Fprintln(os.Stderr, "build failed: "+cmd) os.Exit(1) } } linkBins(clonePath) stageCommit(name, staged) uiOk(name + " updated") } func syncAll() { uiSection("syncing repos") repos := loadRepos() os.MkdirAll(filepath.Dir(db), 0755) var combined strings.Builder for i, repo := range repos { uiInfo(fmt.Sprintf("(%d/%d) %s", i+1, len(repos), repo)) resp, err := http.Get(repo) if err != nil || resp.StatusCode != 200 { uiWarn(fmt.Sprintf("skipped: %s", repo)) continue } data, _ := io.ReadAll(resp.Body) resp.Body.Close() combined.Write(data) combined.WriteString("\n") } os.WriteFile(db, []byte(combined.String()), 0644) uiOk(fmt.Sprintf("synced %d repo(s)", len(repos))) } func bootstrap(recipePath string) { uiSection("bootstrapping BoredOS from " + recipePath) if _, err := os.Stat(recipePath); os.IsNotExist(err) { fmt.Fprintln(os.Stderr, "recipe not found: "+recipePath) os.Exit(1) } deps, buildCmds := parseMolpkg(filepath.Dir(recipePath)) uiInfo("installing bootstrap deps…") for _, d := range deps { install(d, false, false) } uiInfo("running bootstrap build steps…") for _, cmd := range buildCmds { parts := strings.Fields(cmd) uiDim("$ " + cmd) if !run(parts[0], parts[1:], "") { fmt.Fprintln(os.Stderr, "bootstrap step failed: "+cmd) os.Exit(1) } } uiOk("bootstrap complete") } func contains(args []string, flag string) bool { for _, a := range args { if a == flag { return true } } return false } func main() { os.MkdirAll(src, 0755) os.MkdirAll(stage, 0755) os.MkdirAll(manifestDir, 0755) os.MkdirAll(filepath.Dir(db), 0755) args := os.Args[1:] cmd := "" if len(args) > 0 { cmd = args[0] } switch cmd { case "install": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: mol install [--source] [-f]") os.Exit(1) } install(args[1], contains(args, "--source"), contains(args, "-f")) case "remove": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: mol remove ") os.Exit(1) } removePkg(args[1]) case "update": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: mol update [-f]") os.Exit(1) } updatePkg(args[1], contains(args, "-f")) case "sync": syncAll() case "repo": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: mol repo ") os.Exit(1) } switch args[1] { case "add": if len(args) < 3 { fmt.Fprintln(os.Stderr, "usage: mol repo add ") os.Exit(1) } addRepo(args[2]) case "list": for i, r := range loadRepos() { fmt.Printf(" %d. %s\n", i+1, r) } } case "bootstrap": if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: mol bootstrap ") os.Exit(1) } bootstrap(args[1]) case "-l": if _, err := os.Stat(db); os.IsNotExist(err) { fmt.Fprintln(os.Stderr, "no repo — run: mol sync") os.Exit(1) } f, _ := os.Open(db) defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.TrimSpace(scanner.Text()) if l == "" || strings.HasPrefix(l, "#") { continue } fmt.Println(strings.Fields(l)[0]) } case "--version", "-v": uiHeader() fmt.Printf(" OS : %s\n", goos) fmt.Printf(" arch : %s\n", runtime.GOARCH) default: uiHeader() fmt.Printf(` %sUsage:%s mol [options] %sPackage commands:%s install [--source] [-f] install a package remove remove a package update [-f] pull, rebuild, re-link sync fetch all configured repos %sRepo commands:%s repo add register an external repo repo list show configured repos %sInfo:%s -l list available packages --version show version and platform info `, bold, reset, cyan, reset, cyan, reset, cyan, reset, cyan, reset) } }