// Package renode installs the Renode emulator (AUR), arm-none-eabi-gdb, // the arduino-sim helper script, and wokwi-cli. package renode import ( "bytes" "context" "embed" "fmt" "os" "path/filepath" "text/template" "gitlab.hive-brain.de/MDietermann/Archduino/internal/config" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" "gitlab.hive-brain.de/MDietermann/Archduino/internal/platform" ) //go:embed assets/arduino-sim.tmpl var assetsFS embed.FS // Module installs Renode and adjacent emulator tooling. type Module struct { cfg config.RenodeConfig pm platform.PackageManager aur platform.AURHelper } // New constructs a renode Module. func New(cfg config.RenodeConfig, pm platform.PackageManager, aur platform.AURHelper) Module { return Module{cfg: cfg, pm: pm, aur: aur} } type state struct { RenodeInstalled bool GdbInstalled bool SimScriptCurrent bool WokwiInstalled bool RenderedSim []byte SimPath string WokwiPath string } // Meta returns the module metadata. func (Module) Meta() module.Meta { return module.Meta{ID: "renode", Name: "Renode emulator", Description: "Install Renode, ARM GDB, arduino-sim helper, and Wokwi CLI."} } func binPath() (string, error) { h, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(h, ".local", "bin"), nil } func (m Module) renderSim() ([]byte, error) { tmplBytes, err := assetsFS.ReadFile("assets/arduino-sim.tmpl") if err != nil { return nil, err } tmpl, err := template.New("sim").Parse(string(tmplBytes)) if err != nil { return nil, err } var buf bytes.Buffer if err := tmpl.Execute(&buf, m.cfg); err != nil { return nil, err } return buf.Bytes(), nil } // Detect inspects the system and returns current state. func (m Module) Detect(ctx context.Context) (module.State, error) { bin, err := binPath() if err != nil { return nil, err } s := state{ SimPath: filepath.Join(bin, "arduino-sim"), WokwiPath: filepath.Join(bin, "wokwi-cli"), } if ok, err := m.pm.IsInstalled(ctx, "renode-bin"); err == nil && ok { s.RenodeInstalled = true } if ok, err := m.pm.IsInstalled(ctx, "arm-none-eabi-gdb"); err == nil && ok { s.GdbInstalled = true } rendered, err := m.renderSim() if err != nil { return nil, err } s.RenderedSim = rendered if cur, err := os.ReadFile(s.SimPath); err == nil && bytes.Equal(cur, rendered) { s.SimScriptCurrent = true } if _, err := os.Stat(s.WokwiPath); err == nil { s.WokwiInstalled = true } return s, nil } // Plan computes the set of actions needed to converge to desired state. func (m Module) Plan(ctx context.Context, cur module.State) (module.Plan, error) { s, _ := cur.(state) var actions []module.Action if !s.RenodeInstalled { actions = append(actions, module.Action{ Description: "ensure yay installed", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.aur.EnsureInstalled(ctx) }, }) actions = append(actions, module.Action{ Description: "yay install renode-bin", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.aur.Install(ctx, []string{"renode-bin"}) }, }) } if !s.GdbInstalled { actions = append(actions, module.Action{ Description: "pacman install arm-none-eabi-gdb", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.pm.Install(ctx, []string{"arm-none-eabi-gdb"}) }, }) } if !s.SimScriptCurrent { rendered := append([]byte{}, s.RenderedSim...) path := s.SimPath actions = append(actions, module.Action{ Description: "write arduino-sim helper", Kind: module.ActionFileWrite, Exec: func(ctx context.Context) error { return platform.WriteUserFile(path, rendered, 0o755) }, }) } if !s.WokwiInstalled { url := m.cfg.WokwiURL dst := s.WokwiPath actions = append(actions, module.Action{ Description: "download wokwi-cli " + m.cfg.WokwiVersion, Kind: module.ActionFileWrite, Exec: func(ctx context.Context) error { if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { return err } return platform.Download(ctx, url, dst, 0o755) }, }) } return module.Plan{Actions: actions}, nil } // Apply executes the planned actions in order. func (m Module) Apply(ctx context.Context, p module.Plan, r module.Reporter) error { for i, a := range p.Actions { r.Progress(m.Meta().ID, i+1, len(p.Actions), a.Description) if err := a.Exec(ctx); err != nil { return err } } return nil } // Verify re-Detects and errors if state hasn't converged. func (m Module) Verify(ctx context.Context) error { st, err := m.Detect(ctx) if err != nil { return err } s := st.(state) if !s.RenodeInstalled { return fmt.Errorf("renode-bin not installed") } if !s.GdbInstalled { return fmt.Errorf("arm-none-eabi-gdb not installed") } if !s.SimScriptCurrent { return fmt.Errorf("arduino-sim script drift") } if !s.WokwiInstalled { return fmt.Errorf("wokwi-cli missing") } return nil } func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { bin, err := binPath() if err != nil { return module.Plan{}, err } rendered, err := m.renderSim() if err != nil { return module.Plan{}, err } simPath := filepath.Join(bin, "arduino-sim") wokwiPath := filepath.Join(bin, "wokwi-cli") actions := []module.Action{ { Description: "ensure yay installed", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.aur.EnsureInstalled(ctx) }, }, { Description: "yay -S (force) renode-bin", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.aur.Reinstall(ctx, []string{"renode-bin"}) }, }, { Description: "pacman -S (force) arm-none-eabi-gdb", Kind: module.ActionPackageInstall, Exec: func(ctx context.Context) error { return m.pm.Reinstall(ctx, []string{"arm-none-eabi-gdb"}) }, }, { Description: "rewrite arduino-sim helper (force)", Kind: module.ActionFileWrite, Exec: func(ctx context.Context) error { return platform.WriteUserFile(simPath, rendered, 0o755) }, }, { Description: "redownload wokwi-cli " + m.cfg.WokwiVersion + " (force)", Kind: module.ActionFileWrite, Exec: func(ctx context.Context) error { if err := os.MkdirAll(filepath.Dir(wokwiPath), 0o755); err != nil { return err } return platform.Download(ctx, m.cfg.WokwiURL, wokwiPath, 0o755) }, }, } return module.Plan{Actions: actions}, nil }