Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions internal/orchestrator/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
}

Expand All @@ -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")
Expand Down
17 changes: 15 additions & 2 deletions internal/orchestrator/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Expand Down
2 changes: 2 additions & 0 deletions internal/orchestrator/app/testdata/MissingSketch/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: "An app with only python"
description: "An app with only python"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

print("Hello world!")
22 changes: 13 additions & 9 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -175,7 +176,7 @@ func StartApp(
return
}
provisionStartProgress := float32(0.0)
if appToStart.MainSketchPath != nil {
if _, ok := appToStart.GetSketchPath(); ok {
provisionStartProgress = 10.0
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -1175,7 +1179,7 @@ func compileUploadSketch(
}
initReq := &rpc.InitRequest{
Instance: inst,
SketchPath: sketchPath,
SketchPath: sketchPath.String(),
Profile: profile,
}

Expand Down Expand Up @@ -1215,7 +1219,7 @@ func compileUploadSketch(
compileReq := rpc.CompileRequest{
Instance: inst,
Fqbn: "arduino:zephyr:unoq",
SketchPath: sketchPath,
SketchPath: sketchPath.String(),
BuildPath: buildPath,
Jobs: 2,
}
Expand All @@ -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
}
Expand Down
21 changes: 18 additions & 3 deletions internal/orchestrator/sketch_libs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package orchestrator

import (
"context"
"errors"
"log/slog"
"time"

Expand All @@ -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 {
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions internal/orchestrator/sketch_libs_test.go
Original file line number Diff line number Diff line change
@@ -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)
}