// Package arduinocli installs arduino-cli and its configured cores. package arduinocli import ( "bytes" "context" "errors" "fmt" "os" "path/filepath" "gitlab.hive-brain.de/MDietermann/Archduino/internal/config" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" "gitlab.hive-brain.de/MDietermann/Archduino/internal/platform" ) type Module struct { cfg config.ArduinoCLIConfig runner platform.Runner } func New(cfg config.ArduinoCLIConfig, r platform.Runner) Module { return Module{cfg: cfg, runner: r} } type state struct { BinaryPath string BinaryInstalled bool MissingCores []string } func (Module) Meta() module.Meta { return module.Meta{ID: "arduinocli", Name: "arduino-cli", Description: "Install arduino-cli and cores."} } func binaryPath() (string, error) { h, err := os.UserHomeDir() if err != nil { return "", err } return filepath.Join(h, ".local", "bin", "arduino-cli"), nil } func (m Module) Detect(ctx context.Context) (module.State, error) { bp, err := binaryPath() if err != nil { return nil, err } s := state{BinaryPath: bp} if _, err := os.Stat(bp); err == nil { s.BinaryInstalled = true out, _ := m.runner.Output(ctx, bp, "core", "list") for _, core := range m.cfg.Cores { if !bytes.Contains(out, []byte(core)) { s.MissingCores = append(s.MissingCores, core) } } } else { s.MissingCores = append([]string{}, m.cfg.Cores...) } return s, nil } func (m Module) Plan(ctx context.Context, cur module.State) (module.Plan, error) { s, _ := cur.(state) var actions []module.Action if !s.BinaryInstalled { bp := s.BinaryPath actions = append(actions, module.Action{ Description: "download + install arduino-cli " + m.cfg.Version, Kind: module.ActionFileWrite, Exec: func(ctx context.Context) error { tmpDir, err := os.MkdirTemp("", "arduinocli-") if err != nil { return err } defer os.RemoveAll(tmpDir) archive := filepath.Join(tmpDir, "arduino-cli.tar.gz") if err := platform.Download(ctx, m.cfg.URL, archive, 0o644); err != nil { return err } if err := platform.ExtractTarGz(ctx, archive, tmpDir); err != nil { return err } if err := os.MkdirAll(filepath.Dir(bp), 0o755); err != nil { return err } data, err := os.ReadFile(filepath.Join(tmpDir, "arduino-cli")) if err != nil { return err } return os.WriteFile(bp, data, 0o755) }, }) } for _, core := range s.MissingCores { core := core actions = append(actions, module.Action{ Description: "arduino-cli core update-index (for " + core + ")", Kind: module.ActionCommand, Exec: func(ctx context.Context) error { return m.runner.Run(ctx, s.BinaryPath, "core", "update-index") }, }) actions = append(actions, module.Action{ Description: "arduino-cli core install " + core, Kind: module.ActionCommand, Exec: func(ctx context.Context) error { return m.runner.Run(ctx, s.BinaryPath, "core", "install", core) }, }) } return module.Plan{Actions: actions}, nil } 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 } func (m Module) Verify(ctx context.Context) error { st, err := m.Detect(ctx) if err != nil { return err } s := st.(state) if !s.BinaryInstalled { return fmt.Errorf("arduino-cli binary missing") } if len(s.MissingCores) > 0 { return fmt.Errorf("cores missing: %v", s.MissingCores) } return nil } func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { return module.Plan{}, errors.New("PlanForce not implemented") }