Skip to content

Commit f1877ef

Browse files
Roberto Soracmaglie
Roberto Sora
andauthored
Reintroduce the --input-file flag for the upload command (#777)
* Remove deprecation from importFile param * Implement --input-file flag * Add comment in --input-file splitting step * Add e2e test for --input-x flags * Refine upload flags testing * Add --input-file file existence check * Use TrimSuffix instead of replace * Restore -i shorthand flag for --input-file and add CLI checkFlagsConflicts function * Improved build path and project name auto-detection This should make the upload command compatibile with all the reasonable usages. See #764 See #641 * Made UploadTest more resilient * upload: sketch is ignored if input-dir or input-file is specified There is no point in overriding the sketch name if the user explicitly give it via command line. * Update go-paths-helper to version 1.3.2 fixes EquivalentTo when used with abs paths * fix TestGetCommandLine * 100% coverage on detectSketchNameFromBuildPath function * Do not git-ignore all *.bin but just inside the client_example folder * slightly simplified function signature (cosmetic) Co-authored-by: Cristian Maglie <[email protected]>
1 parent 5045656 commit f1877ef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+352
-97
lines changed

Diff for: .gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ venv
1515

1616
# gRPC client example folder
1717
/client_example/client_example
18-
*.bin
19-
*.elf
18+
/client_example/**/*.bin
19+
/client_example/**/*.elf
2020

2121
# Misc.
2222
.DS_Store

Diff for: arduino/sketches/sketches.go

+9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121

2222
"github.com/arduino/go-paths-helper"
23+
"github.com/pkg/errors"
2324
)
2425

2526
// Sketch is a sketch for Arduino
@@ -43,9 +44,17 @@ type BoardMetadata struct {
4344

4445
// NewSketchFromPath loads a sketch from the specified path
4546
func NewSketchFromPath(path *paths.Path) (*Sketch, error) {
47+
path, err := path.Abs()
48+
if err != nil {
49+
return nil, errors.Errorf("getting sketch path: %s", err)
50+
}
4651
if !path.IsDir() {
4752
path = path.Parent()
4853
}
54+
sketchFile := path.Join(path.Base() + ".ino")
55+
if !sketchFile.Exist() {
56+
return nil, errors.Errorf("no valid sketch found in %s: missing %s", path, sketchFile.Base())
57+
}
4958
sketch := &Sketch{
5059
FullPath: path,
5160
Name: path.Base(),

Diff for: cli/upload/upload.go

+11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var (
3636
verbose bool
3737
verify bool
3838
importDir string
39+
importFile string
3940
programmer string
4041
)
4142

@@ -47,19 +48,28 @@ func NewCommand() *cobra.Command {
4748
Long: "Upload Arduino sketches. This does NOT compile the sketch prior to upload.",
4849
Example: " " + os.Args[0] + " upload /home/user/Arduino/MySketch",
4950
Args: cobra.MaximumNArgs(1),
51+
PreRun: checkFlagsConflicts,
5052
Run: run,
5153
}
5254

5355
uploadCommand.Flags().StringVarP(&fqbn, "fqbn", "b", "", "Fully Qualified Board Name, e.g.: arduino:avr:uno")
5456
uploadCommand.Flags().StringVarP(&port, "port", "p", "", "Upload port, e.g.: COM10 or /dev/ttyACM0")
5557
uploadCommand.Flags().StringVarP(&importDir, "input-dir", "", "", "Directory containing binaries to upload.")
58+
uploadCommand.Flags().StringVarP(&importFile, "input-file", "i", "", "Binary file to upload.")
5659
uploadCommand.Flags().BoolVarP(&verify, "verify", "t", false, "Verify uploaded binary after the upload.")
5760
uploadCommand.Flags().BoolVarP(&verbose, "verbose", "v", false, "Optional, turns on verbose mode.")
5861
uploadCommand.Flags().StringVarP(&programmer, "programmer", "P", "", "Optional, use the specified programmer to upload or 'list' to list supported programmers.")
5962

6063
return uploadCommand
6164
}
6265

66+
func checkFlagsConflicts(command *cobra.Command, args []string) {
67+
if importFile != "" && importDir != "" {
68+
feedback.Errorf("error: --input-file and --input-dir flags cannot be used together")
69+
os.Exit(errorcodes.ErrBadArgument)
70+
}
71+
}
72+
6373
func run(command *cobra.Command, args []string) {
6474
instance, err := instance.CreateInstance()
6575
if err != nil {
@@ -80,6 +90,7 @@ func run(command *cobra.Command, args []string) {
8090
Port: port,
8191
Verbose: verbose,
8292
Verify: verify,
93+
ImportFile: importFile,
8394
ImportDir: importDir,
8495
Programmer: programmer,
8596
}, os.Stdout, os.Stderr); err != nil {

Diff for: commands/debug/debug_test.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ import (
2626
dbg "github.com/arduino/arduino-cli/rpc/debug"
2727
"github.com/arduino/go-paths-helper"
2828
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
2930
)
3031

31-
var customHardware = paths.New("testdata", "custom_hardware")
32-
var dataDir = paths.New("testdata", "data_dir", "packages")
33-
var sketch = "hello"
34-
var sketchPath = paths.New("testdata", sketch)
35-
3632
func TestGetCommandLine(t *testing.T) {
33+
customHardware := paths.New("testdata", "custom_hardware")
34+
dataDir := paths.New("testdata", "data_dir", "packages")
35+
sketch := "hello"
36+
sketchPath := paths.New("testdata", sketch)
37+
require.NoError(t, sketchPath.ToAbs())
38+
3739
pm := packagemanager.NewPackageManager(nil, nil, nil, nil)
3840
pm.LoadHardwareFromDirectory(customHardware)
3941
pm.LoadHardwareFromDirectory(dataDir)
@@ -59,9 +61,9 @@ func TestGetCommandLine(t *testing.T) {
5961
fmt.Sprintf(" -c \"gdb_port pipe\" -c \"telnet_port 0\" -c init -c halt %s/build/arduino-test.samd.arduino_zero_edbg/hello.ino.elf", sketchPath)
6062

6163
command, err := getCommandLine(req, pm)
62-
assert.Nil(t, err)
64+
require.Nil(t, err)
6365
commandToTest := strings.Join(command[:], " ")
64-
assert.Equal(t, filepath.FromSlash(goldCommand), filepath.FromSlash(commandToTest))
66+
require.Equal(t, filepath.FromSlash(goldCommand), filepath.FromSlash(commandToTest))
6567

6668
// Other samd boards such as mkr1000 can be debugged using an external tool such as Atmel ICE connected to
6769
// the board debug port
@@ -83,5 +85,4 @@ func TestGetCommandLine(t *testing.T) {
8385
assert.Nil(t, err)
8486
commandToTest2 := strings.Join(command2[:], " ")
8587
assert.Equal(t, filepath.FromSlash(goldCommand2), filepath.FromSlash(commandToTest2))
86-
8788
}

Diff for: commands/debug/testdata/hello/hello.ino

Whitespace-only changes.

Diff for: commands/upload/burnbootloader.go

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func BurnBootloader(ctx context.Context, req *rpc.BurnBootloaderReq, outStream i
3737
err := runProgramAction(
3838
pm,
3939
nil, // sketch
40+
"", // importFile
4041
"", // importDir
4142
req.GetFqbn(),
4243
req.GetPort(),

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/testdata/build_path_4/some_other_files.txt

Whitespace-only changes.

Diff for: commands/upload/upload.go

+110-21
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
)
@@ -42,12 +44,9 @@ func Upload(ctx context.Context, req *rpc.UploadReq, outStream io.Writer, errStr
4244

4345
// TODO: make a generic function to extract sketch from request
4446
// and remove duplication in commands/compile.go
45-
if req.GetSketchPath() == "" {
46-
return nil, fmt.Errorf("missing sketchPath")
47-
}
4847
sketchPath := paths.New(req.GetSketchPath())
4948
sketch, err := sketches.NewSketchFromPath(sketchPath)
50-
if err != nil {
49+
if err != nil && req.GetImportDir() == "" && req.GetImportFile() == "" {
5150
return nil, fmt.Errorf("opening sketch: %s", err)
5251
}
5352

@@ -56,6 +55,7 @@ func Upload(ctx context.Context, req *rpc.UploadReq, outStream io.Writer, errStr
5655
err = runProgramAction(
5756
pm,
5857
sketch,
58+
req.GetImportFile(),
5959
req.GetImportDir(),
6060
req.GetFqbn(),
6161
req.GetPort(),
@@ -73,10 +73,11 @@ func Upload(ctx context.Context, req *rpc.UploadReq, outStream io.Writer, errStr
7373
}
7474

7575
func runProgramAction(pm *packagemanager.PackageManager,
76-
sketch *sketches.Sketch, importDir string, fqbnIn string, port string,
76+
sketch *sketches.Sketch,
77+
importFile, importDir, fqbnIn, port string,
7778
programmerID string,
7879
verbose, verify, burnBootloader bool,
79-
outStream io.Writer, errStream io.Writer) error {
80+
outStream, errStream io.Writer) error {
8081

8182
if burnBootloader && programmerID == "" {
8283
return fmt.Errorf("no programmer specified for burning bootloader")
@@ -239,30 +240,19 @@ func runProgramAction(pm *packagemanager.PackageManager,
239240
uploadProperties.Set("program.verify", uploadProperties.Get("program.params.noverify"))
240241
}
241242

242-
var importPath *paths.Path
243243
if !burnBootloader {
244-
if sketch == nil {
245-
return fmt.Errorf(("no sketch specified"))
246-
}
247-
248-
if importDir != "" {
249-
importPath = paths.New(importDir)
250-
} else {
251-
// TODO: Create a function to obtain importPath from sketch
252-
importPath = sketch.FullPath
253-
// Add FQBN (without configs part) to export path
254-
fqbnSuffix := strings.Replace(fqbn.StringWithoutConfig(), ":", ".", -1)
255-
importPath = importPath.Join("build").Join(fqbnSuffix)
244+
importPath, sketchName, err := determineBuildPathAndSketchName(importFile, importDir, sketch, fqbn)
245+
if err != nil {
246+
return errors.Errorf("retrieving build artifacts: %s", err)
256247
}
257-
258248
if !importPath.Exist() {
259249
return fmt.Errorf("compiled sketch not found in %s", importPath)
260250
}
261251
if !importPath.IsDir() {
262252
return fmt.Errorf("expected compiled sketch in directory %s, but is a file instead", importPath)
263253
}
264254
uploadProperties.SetPath("build.path", importPath)
265-
uploadProperties.Set("build.project_name", sketch.Name+".ino")
255+
uploadProperties.Set("build.project_name", sketchName)
266256
}
267257

268258
// If not using programmer perform some action required
@@ -449,3 +439,102 @@ func waitForNewSerialPort() (string, error) {
449439

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

0 commit comments

Comments
 (0)