Skip to content

Commit de6c443

Browse files
committed
Improved build path and project name auto-detection
This should make the upload command compatibile with all the reasonable usages. See #764 See #641
1 parent 3df5b6b commit de6c443

23 files changed

+225
-35
lines changed

Diff for: commands/upload/testdata/Blonk/Blonk.ino

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.elf

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.hex

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.map

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.with_bootloader.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/Blonk/build/arduino.samd.mkr1000/Blonk.ino.with_bootloader.hex

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_1/sketch.ino.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.elf

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.hex

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.map

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.with_bootloader.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_2/Blink.ino.with_bootloader.hex

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/AnotherSketch.ino.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.elf

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.hex

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.map

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.with_bootloader.bin

Whitespace-only changes.

Diff for: commands/upload/testdata/build_path_3/Blink.ino.with_bootloader.hex

Whitespace-only changes.

Diff for: commands/upload/upload.go

+119-35
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"net/url"
23+
"path/filepath"
2324
"strings"
2425
"time"
2526

@@ -32,6 +33,7 @@ import (
3233
rpc "github.com/arduino/arduino-cli/rpc/commands"
3334
paths "github.com/arduino/go-paths-helper"
3435
properties "github.com/arduino/go-properties-orderedmap"
36+
"github.com/pkg/errors"
3537
"github.com/sirupsen/logrus"
3638
"go.bug.st/serial"
3739
)
@@ -240,43 +242,19 @@ func runProgramAction(pm *packagemanager.PackageManager,
240242
uploadProperties.Set("program.verify", uploadProperties.Get("program.params.noverify"))
241243
}
242244

243-
var importPath *paths.Path
244245
if !burnBootloader {
245-
if importFile != "" {
246-
importFilePath := paths.New(importFile)
247-
if !importFilePath.Exist() {
248-
return fmt.Errorf("binary file not found in %s", importFilePath)
249-
}
250-
importPath = importFilePath.Parent()
251-
// In general, the binary file extension (like .bin or .hex or even .zip) are already written explicitly in
252-
// the core recipes inside platform.txt. This why the CLI removes it before setting the build.project_name
253-
// property.
254-
importFileName := strings.TrimSuffix(importFilePath.Base(), importFilePath.Ext())
255-
uploadProperties.SetPath("build.path", importPath)
256-
uploadProperties.Set("build.project_name", importFileName)
257-
} else {
258-
if sketch == nil {
259-
return fmt.Errorf(("no sketch specified"))
260-
}
261-
if importDir != "" {
262-
importPath = paths.New(importDir)
263-
} else {
264-
// TODO: Create a function to obtain importPath from sketch
265-
importPath = sketch.FullPath
266-
// Add FQBN (without configs part) to export path
267-
fqbnSuffix := strings.Replace(fqbn.StringWithoutConfig(), ":", ".", -1)
268-
importPath = importPath.Join("build").Join(fqbnSuffix)
269-
}
270-
271-
if !importPath.Exist() {
272-
return fmt.Errorf("compiled sketch not found in %s", importPath)
273-
}
274-
if !importPath.IsDir() {
275-
return fmt.Errorf("expected compiled sketch in directory %s, but is a file instead", importPath)
276-
}
277-
uploadProperties.SetPath("build.path", importPath)
278-
uploadProperties.Set("build.project_name", sketch.Name+".ino")
246+
importPath, sketchName, err := determineBuildPathAndSketchName(importFile, importDir, sketch, fqbn)
247+
if err != nil {
248+
return errors.Errorf("retrieving build artifacts: %s", err)
249+
}
250+
if !importPath.Exist() {
251+
return fmt.Errorf("compiled sketch not found in %s", importPath)
252+
}
253+
if !importPath.IsDir() {
254+
return fmt.Errorf("expected compiled sketch in directory %s, but is a file instead", importPath)
279255
}
256+
uploadProperties.SetPath("build.path", importPath)
257+
uploadProperties.Set("build.project_name", sketchName)
280258
}
281259

282260
// If not using programmer perform some action required
@@ -463,3 +441,109 @@ func waitForNewSerialPort() (string, error) {
463441

464442
return "", nil
465443
}
444+
445+
func determineBuildPathAndSketchName(importFile, importDir string, sketch *sketches.Sketch, fqbn *cores.FQBN) (*paths.Path, string, error) {
446+
// In general, compiling a sketch will produce a set of files that are
447+
// named as the sketch but have different extensions, for example Sketch.ino
448+
// may produce: Sketch.ino.bin; Sketch.ino.hex; Sketch.ino.zip; etc...
449+
// These files are created together in the build directory and anyone of
450+
// them may be required for upload.
451+
452+
// The upload recipes are already written using the 'build.project_name' property
453+
// concatenated with an explicit extension. To perform the upload we should now
454+
// determine the project name (e.g. 'sketch.ino) and set the 'build.project_name'
455+
// property accordingly, together with the 'build.path' property to point to the
456+
// directory containing the build artifacts.
457+
458+
// Case 1: importFile flag has been specified
459+
if importFile != "" {
460+
if importDir != "" {
461+
return nil, "", fmt.Errorf("importFile and importDir cannot be used together")
462+
}
463+
464+
// We have a path like "path/to/my/build/SketchName.ino.bin". We are going to
465+
// ignore the extension and set:
466+
// - "build.path" as "path/to/my/build"
467+
// - "build.project_name" as "SketchName.ino"
468+
469+
importFilePath := paths.New(importFile)
470+
if !importFilePath.Exist() {
471+
return nil, "", fmt.Errorf("binary file not found in %s", importFilePath)
472+
}
473+
return importFilePath.Parent(), strings.TrimSuffix(importFilePath.Base(), importFilePath.Ext()), nil
474+
}
475+
476+
if importDir != "" {
477+
// Case 2: importDir flag with a sketch
478+
if sketch != nil {
479+
// In this case we have both the build path and the sketch name given,
480+
// so we just return them as-is
481+
return paths.New(importDir), sketch.Name + ".ino", nil
482+
}
483+
484+
// Case 3: importDir flag without a sketch
485+
486+
// In this case we have a build path but the sketch name is not given, we may
487+
// try to determine the sketch name by applying some euristics to the build folder.
488+
// - "build.path" as importDir
489+
// - "build.project_name" after trying to autodetect it from the build folder.
490+
buildPath := paths.New(importDir)
491+
sketchName, err := detectSketchNameFromBuildPath(buildPath)
492+
if err != nil {
493+
return nil, "", errors.Errorf("autodetect build artifact: %s", err)
494+
}
495+
return buildPath, sketchName, nil
496+
}
497+
498+
// Case 4: nothing given...
499+
if sketch == nil {
500+
return nil, "", fmt.Errorf("no sketch or build directory/file specified")
501+
}
502+
503+
// Case 5: only sketch specified. In this case we use the default sketch build path
504+
// and the given sketch name.
505+
506+
// TODO: Create a function to obtain importPath from sketch
507+
// Add FQBN (without configs part) to export path
508+
if fqbn == nil {
509+
return nil, "", fmt.Errorf("missing FQBN")
510+
}
511+
fqbnSuffix := strings.Replace(fqbn.StringWithoutConfig(), ":", ".", -1)
512+
return sketch.FullPath.Join("build").Join(fqbnSuffix), sketch.Name + ".ino", nil
513+
}
514+
515+
func detectSketchNameFromBuildPath(buildPath *paths.Path) (string, error) {
516+
files, err := buildPath.ReadDir()
517+
if err != nil {
518+
return "", err
519+
}
520+
521+
candidateName := ""
522+
var candidateFile *paths.Path
523+
for _, file := range files {
524+
// Build artifacts are usually names as "Blink.ino.hex" or "Blink.ino.bin".
525+
// Extract the "Blink.ino" part
526+
name := strings.TrimSuffix(file.Base(), file.Ext())
527+
528+
// Sometimes we may have particular files like:
529+
// Blink.ino.with_bootloader.bin
530+
if filepath.Ext(name) != ".ino" {
531+
// just ignore those files
532+
continue
533+
}
534+
535+
if candidateName == "" {
536+
candidateName = name
537+
candidateFile = file
538+
}
539+
540+
if candidateName != name {
541+
return "", errors.Errorf("multiple build artifacts found: '%s' and '%s'", candidateFile, file)
542+
}
543+
}
544+
545+
if candidateName == "" {
546+
return "", errors.New("could not find a valid build artifact")
547+
}
548+
return candidateName, nil
549+
}

Diff for: commands/upload/upload_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 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 [email protected].
15+
16+
package upload
17+
18+
import (
19+
"fmt"
20+
"testing"
21+
22+
"github.com/arduino/arduino-cli/arduino/cores"
23+
"github.com/arduino/arduino-cli/arduino/sketches"
24+
paths "github.com/arduino/go-paths-helper"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestDetectSketchNameFromBuildPath(t *testing.T) {
29+
sk1, err1 := detectSketchNameFromBuildPath(paths.New("testdata/build_path_1"))
30+
require.NoError(t, err1)
31+
require.Equal(t, "sketch.ino", sk1)
32+
33+
sk2, err2 := detectSketchNameFromBuildPath(paths.New("testdata/build_path_2"))
34+
require.NoError(t, err2)
35+
require.Equal(t, "Blink.ino", sk2)
36+
37+
sk3, err3 := detectSketchNameFromBuildPath(paths.New("testdata/build_path_3"))
38+
require.Error(t, err3)
39+
require.Equal(t, "", sk3)
40+
}
41+
42+
func TestDetermineBuildPathAndSketchName(t *testing.T) {
43+
type test struct {
44+
importFile string
45+
importDir string
46+
sketch *sketches.Sketch
47+
fqbn *cores.FQBN
48+
resBuildPath string
49+
resSketchName string
50+
hasError bool
51+
}
52+
53+
blonk, err := sketches.NewSketchFromPath(paths.New("testdata/Blonk"))
54+
require.NoError(t, err)
55+
56+
fqbn, err := cores.ParseFQBN("arduino:samd:mkr1000")
57+
require.NoError(t, err)
58+
59+
tests := []test{
60+
// 00: error: no data passed in
61+
{"", "", nil, nil, "<nil>", "", true},
62+
// 01: use importFile to detect build.path and project_name
63+
{"testdata/build_path_2/Blink.ino.hex", "", nil, nil, "testdata/build_path_2", "Blink.ino", false},
64+
// 02: use importPath as build.path and project_name
65+
{"", "testdata/build_path_2", nil, nil, "testdata/build_path_2", "Blink.ino", false},
66+
// 03: error: used both importPath and importFile
67+
{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", nil, nil, "<nil>", "", true},
68+
// 04: error: only sketch without FQBN
69+
{"", "", blonk, nil, "<nil>", "", true},
70+
// 05: use importFile to detect build.path and project_name, sketch is ignored.
71+
{"testdata/build_path_2/Blink.ino.hex", "", blonk, nil, "testdata/build_path_2", "Blink.ino", false},
72+
// 06: use importPath as build.path and Blonk as project name (forced by the sketch)
73+
{"", "testdata/build_path_2", blonk, nil, "testdata/build_path_2", "Blonk.ino", false},
74+
// 07: error: used both importPath and importFile
75+
{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", blonk, nil, "<nil>", "", true},
76+
77+
// 08: error: no data passed in
78+
{"", "", nil, fqbn, "<nil>", "", true},
79+
// 09: use importFile to detect build.path and project_name, fqbn ignored
80+
{"testdata/build_path_2/Blink.ino.hex", "", nil, fqbn, "testdata/build_path_2", "Blink.ino", false},
81+
// 10: use importPath as build.path and project_name, fqbn ignored
82+
{"", "testdata/build_path_2", nil, fqbn, "testdata/build_path_2", "Blink.ino", false},
83+
// 11: error: used both importPath and importFile
84+
{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", nil, fqbn, "<nil>", "", true},
85+
// 12: use sketch to determine project name and sketch+fqbn to determine build path
86+
{"", "", blonk, fqbn, "testdata/Blonk/build/arduino.samd.mkr1000", "Blonk.ino", false},
87+
// 13: use importFile to detect build.path and project_name, sketch+fqbn is ignored.
88+
{"testdata/build_path_2/Blink.ino.hex", "", blonk, fqbn, "testdata/build_path_2", "Blink.ino", false},
89+
// 14: use importPath as build.path and Blonk as project name (forced by the sketch), fqbn ignored
90+
{"", "testdata/build_path_2", blonk, fqbn, "testdata/build_path_2", "Blonk.ino", false},
91+
// 15: error: used both importPath and importFile
92+
{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", blonk, fqbn, "<nil>", "", true},
93+
}
94+
for i, test := range tests {
95+
t.Run(fmt.Sprintf("SubTest%02d", i), func(t *testing.T) {
96+
buildPath, sketchName, err := determineBuildPathAndSketchName(test.importFile, test.importDir, test.sketch, test.fqbn)
97+
if test.hasError {
98+
require.Error(t, err)
99+
} else {
100+
require.NoError(t, err)
101+
}
102+
require.Equal(t, test.resBuildPath, fmt.Sprint(buildPath))
103+
require.Equal(t, test.resSketchName, sketchName)
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)