Skip to content

Commit c282cc6

Browse files
Add profile init command
It creates a `sketch.yaml` file at the provided path. A new profile can be added to the file by providing a profile name and FQBN.
1 parent dd621ee commit c282cc6

File tree

8 files changed

+1155
-591
lines changed

8 files changed

+1155
-591
lines changed

commands/service_profile_init.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package commands
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
"sync"
23+
24+
"github.com/arduino/arduino-cli/commands/cmderrors"
25+
"github.com/arduino/arduino-cli/commands/internal/instances"
26+
"github.com/arduino/arduino-cli/internal/arduino/sketch"
27+
"github.com/arduino/arduino-cli/internal/i18n"
28+
"github.com/arduino/arduino-cli/pkg/fqbn"
29+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
30+
"github.com/arduino/go-paths-helper"
31+
)
32+
33+
func (s *arduinoCoreServerImpl) InitProfile(ctx context.Context, req *rpc.InitProfileRequest) (*rpc.InitProfileResponse, error) {
34+
sketchPath := paths.New(req.GetSketchPath())
35+
projectFilePath, err := sketchPath.Join("sketch.yaml").Abs()
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
// Returns an error if the main file is missing from the sketch so there is no need to check if the path exists
41+
sk, err := sketch.New(sketchPath)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
if !projectFilePath.Exist() {
47+
err := projectFilePath.WriteFile([]byte("profiles:\n"))
48+
if err != nil {
49+
return nil, err
50+
}
51+
}
52+
53+
if req.GetProfileName() != "" {
54+
if req.GetFqbn() == "" {
55+
return nil, &cmderrors.MissingFQBNError{}
56+
}
57+
58+
// Check that the profile name is unique
59+
if profile, _ := sk.GetProfile(req.ProfileName); profile != nil {
60+
return nil, fmt.Errorf("%s: the profile already exists", req.ProfileName)
61+
}
62+
63+
pme, release, err := instances.GetPackageManagerExplorer(req.GetInstance())
64+
if err != nil {
65+
return nil, err
66+
}
67+
release = sync.OnceFunc(release)
68+
defer release()
69+
70+
if pme.Dirty() {
71+
return nil, &cmderrors.InstanceNeedsReinitialization{}
72+
}
73+
74+
fqbn, err := fqbn.Parse(req.GetFqbn())
75+
if err != nil {
76+
return nil, &cmderrors.InvalidFQBNError{Cause: err}
77+
}
78+
79+
// Automatically detect the target platform if it is installed on the user's machine
80+
_, targetPlatform, _, _, _, err := pme.ResolveFQBN(fqbn)
81+
if err != nil {
82+
if targetPlatform == nil {
83+
return nil, &cmderrors.PlatformNotFoundError{
84+
Platform: fmt.Sprintf("%s:%s", fqbn.Vendor, fqbn.Architecture),
85+
Cause: errors.New(i18n.Tr("platform not installed")),
86+
}
87+
}
88+
return nil, &cmderrors.InvalidFQBNError{Cause: err}
89+
}
90+
91+
newProfile := &sketch.Profile{Name: req.GetProfileName(), FQBN: req.GetFqbn()}
92+
// TODO: what to do with the PlatformIndexURL?
93+
newProfile.Platforms = append(newProfile.Platforms, &sketch.ProfilePlatformReference{
94+
Packager: targetPlatform.Platform.Package.Name,
95+
Architecture: targetPlatform.Platform.Architecture,
96+
Version: targetPlatform.Version,
97+
})
98+
99+
sk.Project.Profiles = append(sk.Project.Profiles, newProfile)
100+
err = projectFilePath.WriteFile([]byte(sk.Project.AsYaml()))
101+
if err != nil {
102+
return nil, err
103+
}
104+
}
105+
106+
return &rpc.InitProfileResponse{ProjectFilePath: projectFilePath.String()}, nil
107+
}

internal/cli/cli.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/arduino/arduino-cli/internal/cli/lib"
3737
"github.com/arduino/arduino-cli/internal/cli/monitor"
3838
"github.com/arduino/arduino-cli/internal/cli/outdated"
39+
"github.com/arduino/arduino-cli/internal/cli/profile"
3940
"github.com/arduino/arduino-cli/internal/cli/sketch"
4041
"github.com/arduino/arduino-cli/internal/cli/update"
4142
"github.com/arduino/arduino-cli/internal/cli/updater"
@@ -162,6 +163,7 @@ func NewCommand(srv rpc.ArduinoCoreServiceServer) *cobra.Command {
162163
cmd.AddCommand(burnbootloader.NewCommand(srv))
163164
cmd.AddCommand(version.NewCommand(srv))
164165
cmd.AddCommand(feedback.NewCommand())
166+
cmd.AddCommand(profile.NewCommand(srv))
165167

166168
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, i18n.Tr("Print the logs on the standard output."))
167169
cmd.Flag("verbose").Hidden = true

internal/cli/profile/init.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package profile
17+
18+
import (
19+
"context"
20+
"os"
21+
22+
"github.com/arduino/arduino-cli/internal/cli/arguments"
23+
"github.com/arduino/arduino-cli/internal/cli/feedback"
24+
"github.com/arduino/arduino-cli/internal/cli/instance"
25+
"github.com/arduino/arduino-cli/internal/i18n"
26+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
func initInitCommand(srv rpc.ArduinoCoreServiceServer) *cobra.Command {
31+
initCommand := &cobra.Command{
32+
Use: "init",
33+
Short: i18n.Tr("Creates or updates the sketch project file."),
34+
Long: i18n.Tr("Creates or updates the sketch project file."),
35+
Example: "" +
36+
" # " + i18n.Tr("Creates or updates the sketch project file in the current directory.") + "\n" +
37+
" " + os.Args[0] + " profile init\n" +
38+
" " + os.Args[0] + " config init --profile Uno_profile -b arduino:avr:uno",
39+
Args: cobra.MaximumNArgs(1),
40+
Run: func(cmd *cobra.Command, args []string) {
41+
runInitCommand(cmd.Context(), args, srv)
42+
},
43+
}
44+
fqbnArg.AddToCommand(initCommand, srv)
45+
profileArg.AddToCommand(initCommand, srv)
46+
return initCommand
47+
}
48+
49+
func runInitCommand(ctx context.Context, args []string, srv rpc.ArduinoCoreServiceServer) {
50+
path := ""
51+
if len(args) > 0 {
52+
path = args[0]
53+
}
54+
55+
sketchPath := arguments.InitSketchPath(path)
56+
57+
inst := instance.CreateAndInit(ctx, srv)
58+
59+
resp, err := srv.InitProfile(ctx, &rpc.InitProfileRequest{Instance: inst, SketchPath: sketchPath.String(), ProfileName: profileArg.Get(), Fqbn: fqbnArg.String()})
60+
if err != nil {
61+
feedback.Fatal(i18n.Tr("Error initializing the project file: %v", err), feedback.ErrGeneric)
62+
}
63+
feedback.PrintResult(profileResult{ProjectFilePath: resp.GetProjectFilePath()})
64+
}
65+
66+
type profileResult struct {
67+
ProjectFilePath string `json:"project_path"`
68+
}
69+
70+
func (ir profileResult) Data() interface{} {
71+
return ir
72+
}
73+
74+
func (ir profileResult) String() string {
75+
return i18n.Tr("Project file created in: %s", ir.ProjectFilePath)
76+
}

internal/cli/profile/profile.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package profile
17+
18+
import (
19+
"os"
20+
21+
"github.com/arduino/arduino-cli/internal/cli/arguments"
22+
"github.com/arduino/arduino-cli/internal/i18n"
23+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
24+
"github.com/spf13/cobra"
25+
)
26+
27+
var (
28+
fqbnArg arguments.Fqbn // Fully Qualified Board Name, e.g.: arduino:avr:uno.
29+
profileArg arguments.Profile // Name of the profile to add to the project
30+
)
31+
32+
func NewCommand(srv rpc.ArduinoCoreServiceServer) *cobra.Command {
33+
profileCommand := &cobra.Command{
34+
Use: "profile",
35+
Short: i18n.Tr("Arduino profile operations."),
36+
Long: i18n.Tr("Arduino profile operations."),
37+
Example: " " + os.Args[0] + " profile init",
38+
}
39+
40+
profileCommand.AddCommand(initInitCommand(srv))
41+
42+
return profileCommand
43+
}

internal/integrationtest/profiles/profiles_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,106 @@ func TestCompileWithDefaultProfile(t *testing.T) {
150150
jsonOut.Query(".builder_result.build_properties").MustContain(`[ "build.fqbn=arduino:avr:nano" ]`)
151151
}
152152
}
153+
154+
func TestInitProfile(t *testing.T) {
155+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
156+
defer env.CleanUp()
157+
158+
// Init the environment explicitly
159+
_, _, err := cli.Run("core", "update-index")
160+
require.NoError(t, err)
161+
162+
_, _, err = cli.Run("sketch", "new", cli.SketchbookDir().Join("Simple").String())
163+
require.NoError(t, err)
164+
165+
_, _, err = cli.Run("core", "install", "arduino:avr")
166+
require.NoError(t, err)
167+
168+
integrationtest.CLISubtests{
169+
{"NoProfile", initNoProfile},
170+
{"ProfileCorrectFQBN", initWithCorrectFqbn},
171+
{"ProfileWrongFQBN", initWithWrongFqbn},
172+
{"ProfileMissingFQBN", initMissingFqbn},
173+
{"ExistingProfile", initExistingProfile},
174+
}.Run(t, env, cli)
175+
}
176+
177+
func initNoProfile(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
178+
projectFile := cli.SketchbookDir().Join("Simple", "sketch.yaml")
179+
// Create an empty project file
180+
stdout, _, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String())
181+
require.NoError(t, err)
182+
require.Contains(t, string(stdout), "Project file created in: "+projectFile.String())
183+
require.FileExists(t, projectFile.String())
184+
fileContent, err := projectFile.ReadFile()
185+
require.NoError(t, err)
186+
require.Equal(t, "profiles:\n", string(fileContent))
187+
}
188+
189+
func initWithCorrectFqbn(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
190+
projectFile := cli.SketchbookDir().Join("Simple", "sketch.yaml")
191+
// Add a profile with a correct FQBN
192+
_, _, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String(), "-m", "Uno", "-b", "arduino:avr:uno")
193+
require.NoError(t, err)
194+
require.FileExists(t, projectFile.String())
195+
fileContent, err := projectFile.ReadFile()
196+
require.NoError(t, err)
197+
require.Equal(t, "profiles:\n Uno:\n fqbn: arduino:avr:uno\n platforms:\n - platform: arduino:avr (1.8.6)\n libraries:\n\n", string(fileContent))
198+
}
199+
200+
func initWithWrongFqbn(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
201+
// Adding a profile with an incorrect FQBN should return an error
202+
_, stderr, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String(), "-m", "wrong_fqbn", "-b", "foo:bar")
203+
require.Error(t, err)
204+
require.Contains(t, string(stderr), "Invalid FQBN")
205+
}
206+
207+
func initMissingFqbn(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
208+
// Add a profile with no FQBN should return an error
209+
_, stderr, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String(), "-m", "Uno")
210+
require.Error(t, err)
211+
require.Contains(t, string(stderr), "Missing FQBN (Fully Qualified Board Name)")
212+
}
213+
214+
func initExistingProfile(t *testing.T, env *integrationtest.Environment, cli *integrationtest.ArduinoCLI) {
215+
// Adding a profile with a name that already exists should return an error
216+
_, stderr, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String(), "-m", "Uno", "-b", "arduino:avr:uno")
217+
require.Error(t, err)
218+
require.Contains(t, string(stderr), "the profile already exists")
219+
}
220+
221+
func TestInitProfileMissingSketchFile(t *testing.T) {
222+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
223+
defer env.CleanUp()
224+
225+
// Init the environment explicitly
226+
_, _, err := cli.Run("core", "update-index")
227+
require.NoError(t, err)
228+
229+
_, stderr, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String())
230+
require.Error(t, err)
231+
require.Contains(t, string(stderr), "no such file or directory")
232+
233+
err = cli.SketchbookDir().Join("Simple").MkdirAll()
234+
require.NoError(t, err)
235+
_, stderr, err = cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String())
236+
require.Error(t, err)
237+
require.Contains(t, string(stderr), "main file missing from sketch")
238+
}
239+
240+
func TestInitProfilePlatformNotInstalled(t *testing.T) {
241+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
242+
defer env.CleanUp()
243+
244+
// Init the environment explicitly
245+
_, _, err := cli.Run("core", "update-index")
246+
require.NoError(t, err)
247+
248+
_, _, err = cli.Run("sketch", "new", cli.SketchbookDir().Join("Simple").String())
249+
require.NoError(t, err)
250+
251+
// Adding a profile with a name that already exists should return an error
252+
_, stderr, err := cli.Run("profile", "init", cli.SketchbookDir().Join("Simple").String(), "-m", "Uno", "-b", "arduino:avr:uno")
253+
require.Error(t, err)
254+
require.Contains(t, string(stderr), "platform not installed")
255+
}

0 commit comments

Comments
 (0)