diff --git a/internal/orchestrator/app/app.go b/internal/orchestrator/app/app.go index 813ca64c..dff16ed3 100644 --- a/internal/orchestrator/app/app.go +++ b/internal/orchestrator/app/app.go @@ -30,7 +30,7 @@ import ( type ArduinoApp struct { Name string MainPythonFile *paths.Path - MainSketchPath *paths.Path + mainSketchPath *paths.Path FullPath *paths.Path // FullPath is the path to the App folder Descriptor AppDescriptor } @@ -76,10 +76,10 @@ func Load(appPath *paths.Path) (ArduinoApp, error) { if appPath.Join("sketch", "sketch.ino").Exist() { // TODO: check sketch casing? - app.MainSketchPath = appPath.Join("sketch") + app.mainSketchPath = appPath.Join("sketch") } - if app.MainPythonFile == nil && app.MainSketchPath == nil { + if app.MainPythonFile == nil && app.mainSketchPath == nil { return ArduinoApp{}, errors.New("main python file and sketch file missing from app") } @@ -91,6 +91,13 @@ func Load(appPath *paths.Path) (ArduinoApp, error) { return app, nil } +func (a *ArduinoApp) GetSketchPath() (*paths.Path, bool) { + if a == nil || a.mainSketchPath == nil { + return nil, false + } + return a.mainSketchPath, true +} + // GetDescriptorPath returns the path to the app descriptor file (app.yaml or app.yml) func (a *ArduinoApp) GetDescriptorPath() *paths.Path { descriptorFile := a.FullPath.Join("app.yaml") diff --git a/internal/orchestrator/app/app_test.go b/internal/orchestrator/app/app_test.go index db9cc048..d8b36eef 100644 --- a/internal/orchestrator/app/app_test.go +++ b/internal/orchestrator/app/app_test.go @@ -58,9 +58,22 @@ func TestLoad(t *testing.T) { assert.NotNil(t, app.MainPythonFile) assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/python/main.py")), app.MainPythonFile.String()) + sketchPath, ok := app.GetSketchPath() + assert.True(t, ok) + assert.NotNil(t, sketchPath) + assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/sketch")), sketchPath.String()) + }) + + t.Run("it loads an app with misssing sketch folder", func(t *testing.T) { + app, err := Load(paths.New("testdata/MissingSketch")) + assert.NoError(t, err) + assert.NotEmpty(t, app) + + assert.NotNil(t, app.MainPythonFile) - assert.NotNil(t, app.MainSketchPath) - assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/sketch")), app.MainSketchPath.String()) + sketchPath, ok := app.GetSketchPath() + assert.False(t, ok) + assert.Nil(t, sketchPath) }) } diff --git a/internal/orchestrator/app/testdata/MissingSketch/app.yaml b/internal/orchestrator/app/testdata/MissingSketch/app.yaml new file mode 100644 index 00000000..adabfa89 --- /dev/null +++ b/internal/orchestrator/app/testdata/MissingSketch/app.yaml @@ -0,0 +1,2 @@ +name: "An app with only python" +description: "An app with only python" diff --git a/internal/orchestrator/app/testdata/MissingSketch/python/main.py b/internal/orchestrator/app/testdata/MissingSketch/python/main.py new file mode 100644 index 00000000..f353b145 --- /dev/null +++ b/internal/orchestrator/app/testdata/MissingSketch/python/main.py @@ -0,0 +1,2 @@ + +print("Hello world!") \ No newline at end of file diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 841e31f1..797af41d 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -154,7 +154,8 @@ func StartApp( if !yield(StreamMessage{progress: &Progress{Name: "preparing", Progress: 0.0}}) { return } - if appToStart.MainSketchPath != nil { + + if _, ok := appToStart.GetSketchPath(); ok { if !yield(StreamMessage{progress: &Progress{Name: "sketch compiling and uploading", Progress: 0.0}}) { return } @@ -175,7 +176,7 @@ func StartApp( return } provisionStartProgress := float32(0.0) - if appToStart.MainSketchPath != nil { + if _, ok := appToStart.GetSketchPath(); ok { provisionStartProgress = 10.0 } @@ -402,7 +403,7 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp, } }) - if app.MainSketchPath != nil { + if _, ok := app.GetSketchPath(); ok { // Before stopping the microcontroller we want to make sure that the app was running. appStatus, err := getAppStatus(ctx, docker, app) if err != nil { @@ -1162,9 +1163,12 @@ func compileUploadSketch( defer func() { _, _ = srv.Destroy(ctx, &rpc.DestroyRequest{Instance: inst}) }() - sketchPath := arduinoApp.MainSketchPath.String() + sketchPath, ok := arduinoApp.GetSketchPath() + if !ok { + return fmt.Errorf("no sketch path found in the Arduino app") + } buildPath := arduinoApp.SketchBuildPath().String() - sketchResp, err := srv.LoadSketch(ctx, &rpc.LoadSketchRequest{SketchPath: sketchPath}) + sketchResp, err := srv.LoadSketch(ctx, &rpc.LoadSketchRequest{SketchPath: sketchPath.String()}) if err != nil { return err } @@ -1175,7 +1179,7 @@ func compileUploadSketch( } initReq := &rpc.InitRequest{ Instance: inst, - SketchPath: sketchPath, + SketchPath: sketchPath.String(), Profile: profile, } @@ -1215,7 +1219,7 @@ func compileUploadSketch( compileReq := rpc.CompileRequest{ Instance: inst, Fqbn: "arduino:zephyr:unoq", - SketchPath: sketchPath, + SketchPath: sketchPath.String(), BuildPath: buildPath, Jobs: 2, } @@ -1241,12 +1245,12 @@ func compileUploadSketch( slog.Info("Used library " + lib.GetName() + " (" + lib.GetVersion() + ") in " + lib.GetInstallDir()) } - if err := uploadSketchInRam(ctx, w, srv, inst, sketchPath, buildPath); err != nil { + if err := uploadSketchInRam(ctx, w, srv, inst, sketchPath.String(), buildPath); err != nil { slog.Warn("failed to upload in ram mode, trying to configure the board in ram mode, and retry", slog.String("error", err.Error())) if err := configureMicroInRamMode(ctx, w, srv, inst); err != nil { return err } - return uploadSketchInRam(ctx, w, srv, inst, sketchPath, buildPath) + return uploadSketchInRam(ctx, w, srv, inst, sketchPath.String(), buildPath) } return nil } diff --git a/internal/orchestrator/sketch_libs.go b/internal/orchestrator/sketch_libs.go index 3ef3b7ff..cb058924 100644 --- a/internal/orchestrator/sketch_libs.go +++ b/internal/orchestrator/sketch_libs.go @@ -17,6 +17,7 @@ package orchestrator import ( "context" + "errors" "log/slog" "time" @@ -30,6 +31,11 @@ import ( const indexUpdateInterval = 10 * time.Minute func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryReleaseID, addDeps bool) ([]LibraryReleaseID, error) { + sketchPath, ok := app.GetSketchPath() + if !ok { + return []LibraryReleaseID{}, errors.New("cannot add a library. Missing sketch folder") + } + srv := commands.NewArduinoCoreServer() var inst *rpc.Instance if res, err := srv.Create(ctx, &rpc.CreateRequest{}); err != nil { @@ -58,7 +64,7 @@ func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryRel resp, err := srv.ProfileLibAdd(ctx, &rpc.ProfileLibAddRequest{ Instance: inst, - SketchPath: app.MainSketchPath.String(), + SketchPath: sketchPath.String(), Library: &rpc.SketchProfileLibraryReference{ Library: &rpc.SketchProfileLibraryReference_IndexLibrary_{ IndexLibrary: &rpc.SketchProfileLibraryReference_IndexLibrary{ @@ -77,6 +83,10 @@ func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryRel } func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryReleaseID) (LibraryReleaseID, error) { + sketchPath, ok := app.GetSketchPath() + if !ok { + return LibraryReleaseID{}, errors.New("cannot remove a library. Missing sketch folder") + } srv := commands.NewArduinoCoreServer() var inst *rpc.Instance if res, err := srv.Create(ctx, &rpc.CreateRequest{}); err != nil { @@ -102,7 +112,7 @@ func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef Library }, }, }, - SketchPath: app.MainSketchPath.String(), + SketchPath: sketchPath.String(), }) if err != nil { return LibraryReleaseID{}, err @@ -111,10 +121,15 @@ func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef Library } func ListSketchLibraries(ctx context.Context, app app.ArduinoApp) ([]LibraryReleaseID, error) { + sketchPath, ok := app.GetSketchPath() + if !ok { + return []LibraryReleaseID{}, errors.New("cannot list libraries. Missing sketch folder") + } + srv := commands.NewArduinoCoreServer() resp, err := srv.ProfileLibList(ctx, &rpc.ProfileLibListRequest{ - SketchPath: app.MainSketchPath.String(), + SketchPath: sketchPath.String(), }) if err != nil { return nil, err diff --git a/internal/orchestrator/sketch_libs_test.go b/internal/orchestrator/sketch_libs_test.go new file mode 100644 index 00000000..d863e60e --- /dev/null +++ b/internal/orchestrator/sketch_libs_test.go @@ -0,0 +1,88 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-app-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package orchestrator + +import ( + "context" + "testing" + + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/arduino/arduino-app-cli/internal/orchestrator/app" +) + +func TestListSketchLibraries(t *testing.T) { + t.Run("fail to list libraries if the sketch folder is missing", func(t *testing.T) { + pythonApp, err := app.Load(createTestAppPythonOnly(t)) + require.NoError(t, err) + + libs, err := ListSketchLibraries(context.Background(), pythonApp) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot list libraries. Missing sketch folder") + assert.Empty(t, libs) + }) + + t.Run("fail to add library if the sketch folder is missing", func(t *testing.T) { + pythonApp, err := app.Load(createTestAppPythonOnly(t)) + require.NoError(t, err) + + libs, err := AddSketchLibrary(context.Background(), pythonApp, LibraryReleaseID{}, false) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot add a library. Missing sketch folder") + assert.Empty(t, libs) + }) + + t.Run("fail to remove library if the sketch folder is missing", func(t *testing.T) { + pythonApp, err := app.Load(createTestAppPythonOnly(t)) + require.NoError(t, err) + + id, err := RemoveSketchLibrary(context.Background(), pythonApp, LibraryReleaseID{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot remove a library. Missing sketch folder") + assert.Empty(t, id) + }) +} + +// Helper function to create a test app without sketch path (Python-only) +func createTestAppPythonOnly(t *testing.T) *paths.Path { + tempDir := t.TempDir() + + appYaml := paths.New(tempDir, "app.yaml") + require.NoError(t, appYaml.WriteFile([]byte(` +name: test-python-app +version: 1.0.0 +description: Test Python-only app +`))) + + // Create python directory and file + pythonDir := paths.New(tempDir, "python") + require.NoError(t, pythonDir.MkdirAll()) + + pythonFile := pythonDir.Join("main.py") + require.NoError(t, pythonFile.WriteFile([]byte(` +import time + +def main(): + print("Hello from Python!") + time.sleep(1) + +if __name__ == "__main__": + main() +`))) + return paths.New(tempDir) +}