Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refined the definition of build_cache.path and the --build-path behaviour #2673

Merged
merged 13 commits into from
Sep 18, 2024
Merged
80 changes: 46 additions & 34 deletions commands/service_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (s *arduinoCoreServerImpl) Compile(req *rpc.CompileRequest, stream rpc.Ardu
return errors.New(i18n.Tr("Firmware encryption/signing requires all the following properties to be defined: %s", "build.keys.keychain, build.keys.sign_key, build.keys.encrypt_key"))
}

// Generate or retrieve build path
// Retrieve build path from user arguments
var buildPath *paths.Path
if buildPathArg := req.GetBuildPath(); buildPathArg != "" {
buildPath = paths.New(req.GetBuildPath()).Canonical()
Expand All @@ -170,44 +170,46 @@ func (s *arduinoCoreServerImpl) Compile(req *rpc.CompileRequest, stream rpc.Ardu
}
}
}
if buildPath == nil {
buildPath = sk.DefaultBuildPath()
}
if err = buildPath.MkdirAll(); err != nil {
return &cmderrors.PermissionDeniedError{Message: i18n.Tr("Cannot create build directory"), Cause: err}
}
buildcache.New(buildPath.Parent()).GetOrCreate(buildPath.Base())
// cache is purged after compilation to not remove entries that might be required

defer maybePurgeBuildCache(
s.settings.GetCompilationsBeforeBuildCachePurge(),
s.settings.GetBuildCacheTTL().Abs())

var buildCachePath *paths.Path
if req.GetBuildCachePath() != "" {
p, err := paths.New(req.GetBuildCachePath()).Abs()
if err != nil {
// If no build path has been set by the user:
// - set up the build cache directory
// - set the sketch build path inside the build cache directory.
var coreBuildCachePath *paths.Path
var extraCoreBuildCachePaths paths.PathList
if buildPath == nil {
var buildCachePath *paths.Path
if p := req.GetBuildCachePath(); p != "" { //nolint:staticcheck
buildCachePath = paths.New(p)
} else {
buildCachePath = s.settings.GetBuildCachePath()
}
if err := buildCachePath.ToAbs(); err != nil {
return &cmderrors.PermissionDeniedError{Message: i18n.Tr("Cannot create build cache directory"), Cause: err}
}
buildCachePath = p
} else if p, ok := s.settings.GetBuildCachePath(); ok {
buildCachePath = p
} else {
buildCachePath = paths.TempDir().Join("arduino")
}
if err := buildCachePath.MkdirAll(); err != nil {
return &cmderrors.PermissionDeniedError{Message: i18n.Tr("Cannot create build cache directory"), Cause: err}
}
coreBuildCachePath := buildCachePath.Join("cores")
if err := buildCachePath.MkdirAll(); err != nil {
return &cmderrors.PermissionDeniedError{Message: i18n.Tr("Cannot create build cache directory"), Cause: err}
}
coreBuildCachePath = buildCachePath.Join("cores")

var extraCoreBuildCachePaths paths.PathList
if len(req.GetBuildCacheExtraPaths()) == 0 {
extraCoreBuildCachePaths = s.settings.GetBuildCacheExtraPaths()
} else {
extraCoreBuildCachePaths = paths.NewPathList(req.GetBuildCacheExtraPaths()...)
if len(req.GetBuildCacheExtraPaths()) == 0 {
extraCoreBuildCachePaths = s.settings.GetBuildCacheExtraPaths()
} else {
extraCoreBuildCachePaths = paths.NewPathList(req.GetBuildCacheExtraPaths()...)
}
for i, p := range extraCoreBuildCachePaths {
extraCoreBuildCachePaths[i] = p.Join("cores")
}

buildPath = s.getDefaultSketchBuildPath(sk, buildCachePath)
buildcache.New(buildPath.Parent()).GetOrCreate(buildPath.Base())

// cache is purged after compilation to not remove entries that might be required
defer maybePurgeBuildCache(
s.settings.GetCompilationsBeforeBuildCachePurge(),
s.settings.GetBuildCacheTTL().Abs())
}
for i, p := range extraCoreBuildCachePaths {
extraCoreBuildCachePaths[i] = p.Join("cores")
if err = buildPath.MkdirAll(); err != nil {
return &cmderrors.PermissionDeniedError{Message: i18n.Tr("Cannot create build directory"), Cause: err}
}

if _, err := pme.FindToolsRequiredForBuild(targetPlatform, buildPlatform); err != nil {
Expand Down Expand Up @@ -416,6 +418,16 @@ func (s *arduinoCoreServerImpl) Compile(req *rpc.CompileRequest, stream rpc.Ardu
return nil
}

// getDefaultSketchBuildPath generates the default build directory for a given sketch.
// The sketch build path is inside the build cache path and is unique for each sketch.
// If overriddenBuildCachePath is nil the build cache path is taken from the settings.
func (s *arduinoCoreServerImpl) getDefaultSketchBuildPath(sk *sketch.Sketch, overriddenBuildCachePath *paths.Path) *paths.Path {
if overriddenBuildCachePath == nil {
overriddenBuildCachePath = s.settings.GetBuildCachePath()
}
return overriddenBuildCachePath.Join("sketches", sk.Hash())
}

// maybePurgeBuildCache runs the build files cache purge if the policy conditions are met.
func maybePurgeBuildCache(compilationsBeforePurge uint, cacheTTL time.Duration) {
// 0 means never purge
Expand Down
32 changes: 32 additions & 0 deletions commands/service_compile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This file is part of arduino-cli.
//
// Copyright 2024 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-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 [email protected].

package commands

import (
"testing"

"github.com/arduino/arduino-cli/internal/arduino/sketch"
paths "github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/assert"
)

func TestGenBuildPath(t *testing.T) {
srv := NewArduinoCoreServer().(*arduinoCoreServerImpl)
want := srv.settings.GetBuildCachePath().Join("sketches", "ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
act := srv.getDefaultSketchBuildPath(&sketch.Sketch{FullPath: paths.New("foo")}, nil)
assert.True(t, act.EquivalentTo(want))
assert.Equal(t, "ACBD18DB4CC2F85CEDEF654FCCC4A4D8", (&sketch.Sketch{FullPath: paths.New("foo")}).Hash())
}
8 changes: 4 additions & 4 deletions commands/service_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (s *arduinoCoreServerImpl) Debug(stream rpc.ArduinoCoreService_DebugServer)
defer release()

// Exec debugger
commandLine, err := getCommandLine(debugConfReq, pme)
commandLine, err := s.getDebugCommandLine(debugConfReq, pme)
if err != nil {
return err
}
Expand Down Expand Up @@ -254,9 +254,9 @@ func (s *arduinoCoreServerImpl) Debug(stream rpc.ArduinoCoreService_DebugServer)
return sendResult(&rpc.DebugResponse_Result{})
}

// getCommandLine compose a debug command represented by a core recipe
func getCommandLine(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer) ([]string, error) {
debugInfo, err := getDebugProperties(req, pme, false)
// getDebugCommandLine compose a debug command represented by a core recipe
func (s *arduinoCoreServerImpl) getDebugCommandLine(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer) ([]string, error) {
debugInfo, err := s.getDebugProperties(req, pme, false)
if err != nil {
return nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions commands/service_debug_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (s *arduinoCoreServerImpl) GetDebugConfig(ctx context.Context, req *rpc.Get
return nil, err
}
defer release()
return getDebugProperties(req, pme, false)
return s.getDebugProperties(req, pme, false)
}

// IsDebugSupported checks if the given board/programmer configuration supports debugging.
Expand All @@ -65,7 +65,7 @@ func (s *arduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.I
Programmer: req.GetProgrammer(),
DebugProperties: req.GetDebugProperties(),
}
expectedOutput, err := getDebugProperties(configRequest, pme, true)
expectedOutput, err := s.getDebugProperties(configRequest, pme, true)
var x *cmderrors.FailedDebugError
if errors.As(err, &x) {
return &rpc.IsDebugSupportedResponse{DebuggingSupported: false}, nil
Expand All @@ -81,7 +81,7 @@ func (s *arduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.I
checkFQBN := minimumFQBN.Clone()
checkFQBN.Configs.Remove(config)
configRequest.Fqbn = checkFQBN.String()
checkOutput, err := getDebugProperties(configRequest, pme, true)
checkOutput, err := s.getDebugProperties(configRequest, pme, true)
if err == nil && reflect.DeepEqual(expectedOutput, checkOutput) {
minimumFQBN.Configs.Remove(config)
}
Expand All @@ -92,7 +92,7 @@ func (s *arduinoCoreServerImpl) IsDebugSupported(ctx context.Context, req *rpc.I
}, nil
}

func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer, skipSketchChecks bool) (*rpc.GetDebugConfigResponse, error) {
func (s *arduinoCoreServerImpl) getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Explorer, skipSketchChecks bool) (*rpc.GetDebugConfigResponse, error) {
var (
sketchName string
sketchDefaultFQBN string
Expand All @@ -111,7 +111,7 @@ func getDebugProperties(req *rpc.GetDebugConfigRequest, pme *packagemanager.Expl
}
sketchName = sk.Name
sketchDefaultFQBN = sk.GetDefaultFQBN()
sketchDefaultBuildPath = sk.DefaultBuildPath()
sketchDefaultBuildPath = s.getDefaultSketchBuildPath(sk, nil)
} else {
// Use placeholder sketch data
sketchName = "Sketch"
Expand Down
7 changes: 4 additions & 3 deletions commands/service_debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,16 @@ func TestGetCommandLine(t *testing.T) {
pme, release := pm.NewExplorer()
defer release()

srv := NewArduinoCoreServer().(*arduinoCoreServerImpl)
{
// Check programmer not found
req.Programmer = "not-existent"
_, err := getCommandLine(req, pme)
_, err := srv.getDebugCommandLine(req, pme)
require.Error(t, err)
}

req.Programmer = "edbg"
command, err := getCommandLine(req, pme)
command, err := srv.getDebugCommandLine(req, pme)
require.Nil(t, err)
commandToTest := strings.Join(command, " ")
require.Equal(t, filepath.FromSlash(goldCommand), filepath.FromSlash(commandToTest))
Expand All @@ -97,7 +98,7 @@ func TestGetCommandLine(t *testing.T) {
fmt.Sprintf(" --file \"%s/arduino-test/samd/variants/mkr1000/openocd_scripts/arduino_zero.cfg\"", customHardware) +
fmt.Sprintf(" -c \"gdb_port pipe\" -c \"telnet_port 0\" %s/build/arduino-test.samd.mkr1000/hello.ino.elf", sketchPath)

command2, err := getCommandLine(req2, pme)
command2, err := srv.getDebugCommandLine(req2, pme)
assert.Nil(t, err)
commandToTest2 := strings.Join(command2, " ")
assert.Equal(t, filepath.FromSlash(goldCommand2), filepath.FromSlash(commandToTest2))
Expand Down
10 changes: 5 additions & 5 deletions commands/service_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (s *arduinoCoreServerImpl) Upload(req *rpc.UploadRequest, stream rpc.Arduin
})
})
defer errStream.Close()
updatedPort, err := runProgramAction(
updatedPort, err := s.runProgramAction(
stream.Context(),
pme,
sk,
Expand Down Expand Up @@ -262,7 +262,7 @@ func (s *arduinoCoreServerImpl) UploadUsingProgrammer(req *rpc.UploadUsingProgra
}, streamAdapter)
}

func runProgramAction(ctx context.Context, pme *packagemanager.Explorer,
func (s *arduinoCoreServerImpl) runProgramAction(ctx context.Context, pme *packagemanager.Explorer,
sk *sketch.Sketch,
importFile, importDir, fqbnIn string, userPort *rpc.Port,
programmerID string,
Expand Down Expand Up @@ -443,7 +443,7 @@ func runProgramAction(ctx context.Context, pme *packagemanager.Explorer,
}

if !burnBootloader {
importPath, sketchName, err := determineBuildPathAndSketchName(importFile, importDir, sk)
importPath, sketchName, err := s.determineBuildPathAndSketchName(importFile, importDir, sk)
if err != nil {
return nil, &cmderrors.NotFoundError{Message: i18n.Tr("Error finding build artifacts"), Cause: err}
}
Expand Down Expand Up @@ -746,7 +746,7 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri
return nil
}

func determineBuildPathAndSketchName(importFile, importDir string, sk *sketch.Sketch) (*paths.Path, string, error) {
func (s *arduinoCoreServerImpl) determineBuildPathAndSketchName(importFile, importDir string, sk *sketch.Sketch) (*paths.Path, string, error) {
// In general, compiling a sketch will produce a set of files that are
// named as the sketch but have different extensions, for example Sketch.ino
// may produce: Sketch.ino.bin; Sketch.ino.hex; Sketch.ino.zip; etc...
Expand Down Expand Up @@ -799,7 +799,7 @@ func determineBuildPathAndSketchName(importFile, importDir string, sk *sketch.Sk

// Case 4: only sketch specified. In this case we use the generated build path
// and the given sketch name.
return sk.DefaultBuildPath(), sk.Name + sk.MainFile.Ext(), nil
return s.getDefaultSketchBuildPath(sk, nil), sk.Name + sk.MainFile.Ext(), nil
}

func detectSketchNameFromBuildPath(buildPath *paths.Path) (string, error) {
Expand Down
2 changes: 1 addition & 1 deletion commands/service_upload_burnbootloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *arduinoCoreServerImpl) BurnBootloader(req *rpc.BurnBootloaderRequest, s
}
defer release()

if _, err := runProgramAction(
if _, err := s.runProgramAction(
stream.Context(),
pme,
nil, // sketch
Expand Down
11 changes: 7 additions & 4 deletions commands/service_upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
fqbn, err := cores.ParseFQBN("arduino:samd:mkr1000")
require.NoError(t, err)

srv := NewArduinoCoreServer().(*arduinoCoreServerImpl)

tests := []test{
// 00: error: no data passed in
{"", "", nil, nil, "<nil>", ""},
Expand All @@ -81,7 +83,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
// 03: error: used both importPath and importFile
{"testdata/upload/build_path_2/Blink.ino.hex", "testdata/upload/build_path_2", nil, nil, "<nil>", ""},
// 04: only sketch without FQBN
{"", "", blonk, nil, blonk.DefaultBuildPath().String(), "Blonk.ino"},
{"", "", blonk, nil, srv.getDefaultSketchBuildPath(blonk, nil).String(), "Blonk.ino"},
// 05: use importFile to detect build.path and project_name, sketch is ignored.
{"testdata/upload/build_path_2/Blink.ino.hex", "", blonk, nil, "testdata/upload/build_path_2", "Blink.ino"},
// 06: use importPath as build.path and Blink as project name, ignore the sketch Blonk
Expand All @@ -97,7 +99,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
// 11: error: used both importPath and importFile
{"testdata/upload/build_path_2/Blink.ino.hex", "testdata/upload/build_path_2", nil, fqbn, "<nil>", ""},
// 12: use sketch to determine project name and sketch+fqbn to determine build path
{"", "", blonk, fqbn, blonk.DefaultBuildPath().String(), "Blonk.ino"},
{"", "", blonk, fqbn, srv.getDefaultSketchBuildPath(blonk, nil).String(), "Blonk.ino"},
// 13: use importFile to detect build.path and project_name, sketch+fqbn is ignored.
{"testdata/upload/build_path_2/Blink.ino.hex", "", blonk, fqbn, "testdata/upload/build_path_2", "Blink.ino"},
// 14: use importPath as build.path and Blink as project name, ignore the sketch Blonk, ignore fqbn
Expand All @@ -111,7 +113,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
}
for i, test := range tests {
t.Run(fmt.Sprintf("SubTest%02d", i), func(t *testing.T) {
buildPath, sketchName, err := determineBuildPathAndSketchName(test.importFile, test.importDir, test.sketch)
buildPath, sketchName, err := srv.determineBuildPathAndSketchName(test.importFile, test.importDir, test.sketch)
if test.resBuildPath == "<nil>" {
require.Error(t, err)
require.Nil(t, buildPath)
Expand Down Expand Up @@ -183,10 +185,11 @@ func TestUploadPropertiesComposition(t *testing.T) {
pme, release := pm.NewExplorer()
defer release()

srv := NewArduinoCoreServer().(*arduinoCoreServerImpl)
testRunner := func(t *testing.T, test test, verboseVerify bool) {
outStream := &bytes.Buffer{}
errStream := &bytes.Buffer{}
_, err := runProgramAction(
_, err := srv.runProgramAction(
context.Background(),
pme,
nil, // sketch
Expand Down
27 changes: 26 additions & 1 deletion docs/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
# Upgrading

Here you can find a list of migration guides to handle breaking changes between releases of the CLI.
Here you can find a list of migration guides to handle breaking changes, deprecations, and bugfixes that may cause
problems between releases of the CLI.

## 1.0.4

### The build cache path specified with `compile --build-cache-path` or `build_cache.path` now affects also sketches.

Previously the specified build cache path only affected cores and it was ignored for sketches. This is now fixed and
both cores and sketches are cached in the given directory.

### A full build of the sketch is performed if a build path is specified in `compile --build-path ...`.

Previously if a build path was specified a cached core could have been used from the global build cache path resulting
in a partial build inside the given build path.

Now if a build path is specified, the global build cache path is ignored and the full build is done in the given build
path.

#### `compile --build-cache-path` is deprecated.

The change above, makes the `compile --build-cache-path` flag useless. It is kept just for backward compatibility.

### The default `build_cache.path` has been moved from the temp folder to the user's cache folder.

Previously the `build_cache.path` was in `$TMP/arduino`. Now it has been moved to the specific OS user's cache folder,
for example in Linux it happens to be `$HOME/.cache/arduino`.

## 1.0.0

Expand Down
6 changes: 0 additions & 6 deletions internal/arduino/sketch/sketch.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,6 @@ func (e *InvalidSketchFolderNameError) Error() string {
return i18n.Tr("no valid sketch found in %[1]s: missing %[2]s", e.SketchFolder, e.SketchFile)
}

// DefaultBuildPath generates the default build directory for a given sketch.
// The build path is in a temporary directory and is unique for each sketch.
func (s *Sketch) DefaultBuildPath() *paths.Path {
return paths.TempDir().Join("arduino", "sketches", s.Hash())
}

// Hash generate a unique hash for the given sketch.
func (s *Sketch) Hash() string {
path := s.FullPath.String()
Expand Down
6 changes: 0 additions & 6 deletions internal/arduino/sketch/sketch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,6 @@ func TestNewSketchFolderSymlink(t *testing.T) {
require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("s_file.S")))
}

func TestGenBuildPath(t *testing.T) {
want := paths.TempDir().Join("arduino", "sketches", "ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
assert.True(t, (&Sketch{FullPath: paths.New("foo")}).DefaultBuildPath().EquivalentTo(want))
assert.Equal(t, "ACBD18DB4CC2F85CEDEF654FCCC4A4D8", (&Sketch{FullPath: paths.New("foo")}).Hash())
}

func TestNewSketchWithSymlink(t *testing.T) {
sketchPath, _ := paths.New("testdata", "SketchWithSymlink").Abs()
mainFilePath := sketchPath.Join("SketchWithSymlink.ino")
Expand Down
Loading
Loading