Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4bfe576

Browse files
committedSep 1, 2020
Add archive command to zip a sketch and its files
1 parent ef57e49 commit 4bfe576

File tree

21 files changed

+6052
-315
lines changed

21 files changed

+6052
-315
lines changed
 

‎cli/archive/archive.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 license@arduino.cc.
15+
16+
package archive
17+
18+
import (
19+
"context"
20+
"os"
21+
22+
"github.com/arduino/arduino-cli/cli/errorcodes"
23+
"github.com/arduino/arduino-cli/cli/feedback"
24+
"github.com/arduino/arduino-cli/commands"
25+
rpc "github.com/arduino/arduino-cli/rpc/commands"
26+
"github.com/sirupsen/logrus"
27+
"github.com/spf13/cobra"
28+
)
29+
30+
var includeBuildDir bool
31+
32+
// NewCommand creates a new `archive` command
33+
func NewCommand() *cobra.Command {
34+
command := &cobra.Command{
35+
Use: "archive <sketchPath> <archivePath>",
36+
Short: "Creates a zip file containing all sketch files.",
37+
Long: "Creates a zip file containing all sketch files.",
38+
Example: "" +
39+
" " + os.Args[0] + " archive\n" +
40+
" " + os.Args[0] + " archive .\n" +
41+
" " + os.Args[0] + " archive . MySketchArchive.zip\n" +
42+
" " + os.Args[0] + " archive /home/user/Arduino/MySketch\n" +
43+
" " + os.Args[0] + " archive /home/user/Arduino/MySketch /home/user/MySketchArchive.zip",
44+
Args: cobra.MaximumNArgs(2),
45+
Run: runArchiveCommand,
46+
}
47+
48+
command.Flags().BoolVar(&includeBuildDir, "include-build-dir", false, "Includes build directory in the archive.")
49+
50+
return command
51+
}
52+
53+
func runArchiveCommand(cmd *cobra.Command, args []string) {
54+
logrus.Info("Executing `arduino archive`")
55+
56+
sketchPath := ""
57+
if len(args) >= 1 {
58+
sketchPath = args[0]
59+
}
60+
61+
archivePath := ""
62+
if len(args) == 2 {
63+
archivePath = args[1]
64+
}
65+
66+
_, err := commands.ArchiveSketch(context.Background(),
67+
&rpc.ArchiveSketchReq{
68+
SketchPath: sketchPath,
69+
ArchivePath: archivePath,
70+
IncludeBuildDir: includeBuildDir,
71+
})
72+
73+
if err != nil {
74+
feedback.Errorf("Error archiving: %v", err)
75+
os.Exit(errorcodes.ErrGeneric)
76+
}
77+
}

‎cli/cli.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"strings"
2323

24+
"github.com/arduino/arduino-cli/cli/archive"
2425
"github.com/arduino/arduino-cli/cli/board"
2526
"github.com/arduino/arduino-cli/cli/burnbootloader"
2627
"github.com/arduino/arduino-cli/cli/cache"
@@ -79,6 +80,7 @@ func NewCommand() *cobra.Command {
7980

8081
// this is here only for testing
8182
func createCliCommandTree(cmd *cobra.Command) {
83+
cmd.AddCommand(archive.NewCommand())
8284
cmd.AddCommand(board.NewCommand())
8385
cmd.AddCommand(cache.NewCommand())
8486
cmd.AddCommand(compile.NewCommand())

‎commands/daemon/daemon.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,8 @@ func (s *ArduinoCoreServerImpl) LibrarySearch(ctx context.Context, req *rpc.Libr
337337
func (s *ArduinoCoreServerImpl) LibraryList(ctx context.Context, req *rpc.LibraryListReq) (*rpc.LibraryListResp, error) {
338338
return lib.LibraryList(ctx, req)
339339
}
340+
341+
// ArchiveSketch FIXMEDOC
342+
func (s *ArduinoCoreServerImpl) ArchiveSketch(ctx context.Context, req *rpc.ArchiveSketchReq) (*rpc.ArchiveSketchResp, error) {
343+
return commands.ArchiveSketch(ctx, req)
344+
}

‎commands/instances.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
package commands
1717

1818
import (
19+
"archive/zip"
1920
"context"
2021
"errors"
2122
"fmt"
23+
"io"
2224
"io/ioutil"
2325
"net/url"
26+
"os"
2427
"path"
28+
"strings"
2529

2630
"github.com/arduino/arduino-cli/arduino/builder"
2731
"github.com/arduino/arduino-cli/arduino/cores"
@@ -702,3 +706,146 @@ func LoadSketch(ctx context.Context, req *rpc.LoadSketchReq) (*rpc.LoadSketchRes
702706
AdditionalFiles: additionalFiles,
703707
}, nil
704708
}
709+
710+
// ArchiveSketch FIXMEDOC
711+
func ArchiveSketch(ctx context.Context, req *rpc.ArchiveSketchReq) (*rpc.ArchiveSketchResp, error) {
712+
// sketchName is the name of the sketch without extension, for example "MySketch"
713+
var sketchName string
714+
715+
sketchPath := paths.New(req.SketchPath)
716+
if sketchPath == nil {
717+
sketchPath = paths.New(".")
718+
}
719+
720+
sketchPath, err := sketchPath.Clean().Abs()
721+
if err != nil {
722+
return nil, fmt.Errorf("Error getting absolute sketch path %v", err)
723+
}
724+
725+
// Get the sketch name and make sketchPath point to the ino file
726+
if sketchPath.IsDir() {
727+
sketchName = sketchPath.Base()
728+
sketchPath = sketchPath.Join(sketchName + ".ino")
729+
} else if sketchPath.Ext() == ".ino" {
730+
sketchName = strings.TrimSuffix(sketchPath.Base(), ".ino")
731+
}
732+
733+
// Checks if it's really a sketch
734+
if sketchPath.NotExist() {
735+
return nil, fmt.Errorf("specified path is not a sketch: %v", sketchPath.String())
736+
}
737+
738+
archivePath := paths.New(req.ArchivePath)
739+
if archivePath == nil {
740+
archivePath = sketchPath.Parent().Parent()
741+
}
742+
743+
archivePath, err = archivePath.Clean().Abs()
744+
if err != nil {
745+
return nil, fmt.Errorf("Error getting absolute archive path %v", err)
746+
}
747+
748+
// Makes archivePath point to a zip file
749+
if archivePath.IsDir() {
750+
archivePath = archivePath.Join(sketchName + ".zip")
751+
} else if archivePath.Ext() == "" {
752+
archivePath = paths.New(archivePath.String() + ".zip")
753+
}
754+
755+
if archivePath.Exist() {
756+
return nil, fmt.Errorf("archive already exists")
757+
}
758+
759+
archive, err := os.Create(archivePath.Clean().String())
760+
if err != nil {
761+
return nil, fmt.Errorf("Error creating archive: %v", err)
762+
}
763+
defer archive.Close()
764+
765+
zipWriter := zip.NewWriter(archive)
766+
defer zipWriter.Close()
767+
768+
filesToZip, err := getSketchContent(sketchPath.Parent(), req.IncludeBuildDir)
769+
if err != nil {
770+
return nil, fmt.Errorf("Error retrieving sketch files: %v", err)
771+
}
772+
773+
for _, f := range filesToZip {
774+
err = addFileToSketchArchive(zipWriter, f.String(), sketchPath.Parent().String())
775+
if err != nil {
776+
return nil, fmt.Errorf("Error adding file to archive: %v", err)
777+
}
778+
}
779+
780+
return &rpc.ArchiveSketchResp{}, nil
781+
}
782+
783+
// Recursively retrieves all files in the sketch folder
784+
func getSketchContent(sketchFolder *paths.Path, includeBuildDir bool) (paths.PathList, error) {
785+
sketchFiles, err := sketchFolder.ReadDir()
786+
if err != nil {
787+
return nil, err
788+
}
789+
for _, f := range sketchFiles {
790+
// Skips build dir
791+
if includeBuildDir && f.Base() == "build" {
792+
continue
793+
}
794+
795+
if f.IsDir() {
796+
files, err := getSketchContent(f, includeBuildDir)
797+
if err != nil {
798+
return nil, err
799+
}
800+
801+
sketchFiles = append(sketchFiles, files...)
802+
}
803+
}
804+
finalFiles := paths.PathList{}
805+
for _, f := range sketchFiles {
806+
if f.IsNotDir() {
807+
finalFiles = append(finalFiles, f)
808+
}
809+
}
810+
return finalFiles, nil
811+
}
812+
813+
// Adds a single file to an existing zip file
814+
func addFileToSketchArchive(zipWriter *zip.Writer, filename, sketchFolder string) error {
815+
f, err := os.Open(filename)
816+
if err != nil {
817+
return err
818+
}
819+
defer f.Close()
820+
821+
info, err := f.Stat()
822+
if err != nil {
823+
return err
824+
}
825+
826+
header, err := zip.FileInfoHeader(info)
827+
if err != nil {
828+
return err
829+
}
830+
831+
// We get the parent path since we want the archive to unpack as a folder.
832+
// If we don't do this the archive would contain all the sketch files as top level.
833+
sketchPath := paths.New(sketchFolder).Join("..")
834+
filePath := paths.New(filename)
835+
836+
filePath, err = sketchPath.RelTo(filePath)
837+
if err != nil {
838+
return err
839+
}
840+
841+
header.Name = filePath.String()
842+
header.Method = zip.Deflate
843+
844+
writer, err := zipWriter.CreateHeader(header)
845+
if err != nil {
846+
return err
847+
}
848+
849+
_, err = io.Copy(writer, f)
850+
return err
851+
}

‎rpc/commands/commands.pb.go

Lines changed: 495 additions & 309 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎rpc/commands/commands.proto

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ service ArduinoCore {
6161
// Returns all files composing a Sketch
6262
rpc LoadSketch(LoadSketchReq) returns (LoadSketchResp) {}
6363

64+
// Creates a zip file containing all files of specified Sketch
65+
rpc ArchiveSketch(ArchiveSketchReq) returns (ArchiveSketchResp) {}
66+
6467
// BOARD COMMANDS
6568
// --------------
6669

@@ -221,10 +224,10 @@ message UpgradeReq {
221224
}
222225

223226
message UpgradeResp {
224-
// Progress of the downloads of the platforms and libraries files.
225-
DownloadProgress progress = 1;
226-
// Description of the current stage of the upgrade.
227-
TaskProgress task_progress = 2;
227+
// Progress of the downloads of the platforms and libraries files.
228+
DownloadProgress progress = 1;
229+
// Description of the current stage of the upgrade.
230+
TaskProgress task_progress = 2;
228231
}
229232

230233
message VersionReq {}
@@ -251,3 +254,14 @@ message LoadSketchResp {
251254
// List of absolute paths to additional sketch files
252255
repeated string additional_files = 4;
253256
}
257+
258+
message ArchiveSketchReq{
259+
// Absolute path to Sketch file or folder containing Sketch file
260+
string sketch_path = 1;
261+
// Absolute path to archive that will be created or folder that will contain it
262+
string archive_path = 2;
263+
// Specifies if build directory should be included in the archive
264+
bool include_build_dir = 3;
265+
}
266+
267+
message ArchiveSketchResp { }

‎test/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,12 @@ def run_command(pytestconfig, data_dir, downloads_dir, working_dir):
9292
}
9393
os.makedirs(os.path.join(data_dir, "packages"))
9494

95-
def _run(cmd_string):
95+
def _run(cmd_string, custom_working_dir=None):
96+
if not custom_working_dir:
97+
custom_working_dir = working_dir
9698
cli_full_line = "{} {}".format(cli_path, cmd_string)
9799
run_context = Context()
98-
with run_context.cd(working_dir):
100+
with run_context.cd(custom_working_dir):
99101
return run_context.run(cli_full_line, echo=False, hide=True, warn=True, env=env)
100102

101103
return _run

‎test/test_archive.py

Lines changed: 1117 additions & 0 deletions
Large diffs are not rendered by default.

‎test/testdata/sketch_simple/build/adafruit.samd.adafruit_feather_m0/sketch_simple.ino.hex

Lines changed: 679 additions & 0 deletions
Large diffs are not rendered by default.

‎test/testdata/sketch_simple/build/adafruit.samd.adafruit_feather_m0/sketch_simple.ino.map

Lines changed: 3389 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:00000001FF
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
:100000000C9434000C9446000C9446000C9446006A
2+
:100010000C9446000C9446000C9446000C94460048
3+
:100020000C9446000C9446000C9446000C94460038
4+
:100030000C9446000C9446000C9446000C94460028
5+
:100040000C9448000C9446000C9446000C94460016
6+
:100050000C9446000C9446000C9446000C94460008
7+
:100060000C9446000C94460011241FBECFEFD8E03C
8+
:10007000DEBFCDBF21E0A0E0B1E001C01D92A930FC
9+
:10008000B207E1F70E9492000C94DC000C9400008F
10+
:100090001F920F920FB60F9211242F933F938F93BD
11+
:1000A0009F93AF93BF938091050190910601A0911A
12+
:1000B0000701B09108013091040123E0230F2D378F
13+
:1000C00058F50196A11DB11D2093040180930501EF
14+
:1000D00090930601A0930701B0930801809100015D
15+
:1000E00090910101A0910201B09103010196A11D1F
16+
:1000F000B11D8093000190930101A0930201B09380
17+
:100100000301BF91AF919F918F913F912F910F90DC
18+
:100110000FBE0F901F90189526E8230F0296A11D81
19+
:10012000B11DD2CF789484B5826084BD84B58160DE
20+
:1001300084BD85B5826085BD85B5816085BD8091B2
21+
:100140006E00816080936E0010928100809181002A
22+
:100150008260809381008091810081608093810022
23+
:10016000809180008160809380008091B1008460E4
24+
:100170008093B1008091B00081608093B000809145
25+
:100180007A00846080937A0080917A008260809304
26+
:100190007A0080917A00816080937A0080917A0061
27+
:1001A000806880937A001092C100C0E0D0E0209770
28+
:0C01B000F1F30E940000FBCFF894FFCF99
29+
:00000001FF
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
:020000040000FA
2+
:100000000C9434000C9446000C9446000C9446006A
3+
:100010000C9446000C9446000C9446000C94460048
4+
:100020000C9446000C9446000C9446000C94460038
5+
:100030000C9446000C9446000C9446000C94460028
6+
:100040000C9448000C9446000C9446000C94460016
7+
:100050000C9446000C9446000C9446000C94460008
8+
:100060000C9446000C94460011241FBECFEFD8E03C
9+
:10007000DEBFCDBF21E0A0E0B1E001C01D92A930FC
10+
:10008000B207E1F70E9492000C94DC000C9400008F
11+
:100090001F920F920FB60F9211242F933F938F93BD
12+
:1000A0009F93AF93BF938091050190910601A0911A
13+
:1000B0000701B09108013091040123E0230F2D378F
14+
:1000C00058F50196A11DB11D2093040180930501EF
15+
:1000D00090930601A0930701B0930801809100015D
16+
:1000E00090910101A0910201B09103010196A11D1F
17+
:1000F000B11D8093000190930101A0930201B09380
18+
:100100000301BF91AF919F918F913F912F910F90DC
19+
:100110000FBE0F901F90189526E8230F0296A11D81
20+
:10012000B11DD2CF789484B5826084BD84B58160DE
21+
:1001300084BD85B5826085BD85B5816085BD8091B2
22+
:100140006E00816080936E0010928100809181002A
23+
:100150008260809381008091810081608093810022
24+
:10016000809180008160809380008091B1008460E4
25+
:100170008093B1008091B00081608093B000809145
26+
:100180007A00846080937A0080917A008260809304
27+
:100190007A0080917A00816080937A0080917A0061
28+
:1001A000806880937A001092C100C0E0D0E0209770
29+
:0C01B000F1F30E940000FBCFF894FFCF99
30+
:107E0000112484B714BE81FFF0D085E080938100F7
31+
:107E100082E08093C00088E18093C10086E0809377
32+
:107E2000C20080E18093C4008EE0C9D0259A86E02C
33+
:107E300020E33CEF91E0309385002093840096BBD3
34+
:107E4000B09BFECF1D9AA8958150A9F7CC24DD24C4
35+
:107E500088248394B5E0AB2EA1E19A2EF3E0BF2EE7
36+
:107E6000A2D0813461F49FD0082FAFD0023811F036
37+
:107E7000013811F484E001C083E08DD089C08234E0
38+
:107E800011F484E103C0853419F485E0A6D080C0E4
39+
:107E9000853579F488D0E82EFF2485D0082F10E0AE
40+
:107EA000102F00270E291F29000F111F8ED06801E7
41+
:107EB0006FC0863521F484E090D080E0DECF843638
42+
:107EC00009F040C070D06FD0082F6DD080E0C81688
43+
:107ED00080E7D80618F4F601B7BEE895C0E0D1E017
44+
:107EE00062D089930C17E1F7F0E0CF16F0E7DF06D8
45+
:107EF00018F0F601B7BEE89568D007B600FCFDCFD4
46+
:107F0000A601A0E0B1E02C9130E011968C91119780
47+
:107F100090E0982F8827822B932B1296FA010C0160
48+
:107F200087BEE89511244E5F5F4FF1E0A038BF0790
49+
:107F300051F7F601A7BEE89507B600FCFDCF97BE46
50+
:107F4000E89526C08437B1F42ED02DD0F82E2BD052
51+
:107F50003CD0F601EF2C8F010F5F1F4F84911BD097
52+
:107F6000EA94F801C1F70894C11CD11CFA94CF0C13
53+
:107F7000D11C0EC0853739F428D08EE10CD085E9AC
54+
:107F80000AD08FE07ACF813511F488E018D01DD067
55+
:107F900080E101D065CF982F8091C00085FFFCCF94
56+
:107FA0009093C60008958091C00087FFFCCF809118
57+
:107FB000C00084FD01C0A8958091C6000895E0E648
58+
:107FC000F0E098E1908380830895EDDF803219F02E
59+
:107FD00088E0F5DFFFCF84E1DECF1F93182FE3DFCA
60+
:107FE0001150E9F7F2DF1F91089580E0E8DFEE27F6
61+
:047FF000FF270994CA
62+
:027FFE00040479
63+
:00000001FF

‎test/testdata/sketch_simple/doc.txt

Whitespace-only changes.

‎test/testdata/sketch_simple/header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#define TRUE FALSE
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include <Arduino.h>
2+
#line 1 {{QuoteCppString .sketch.MainFile.Name}}
3+
void setup() {
4+
5+
}
6+
7+
void loop() {
8+
9+
}
10+
#line 1 {{QuoteCppString (index .sketch.OtherSketchFiles 0).Name}}
11+
12+
#line 1 {{QuoteCppString (index .sketch.OtherSketchFiles 1).Name}}
13+
String hello() {
14+
return "world";
15+
}

‎test/testdata/sketch_simple/old.pde

Whitespace-only changes.

‎test/testdata/sketch_simple/other.ino

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
String hello() {
2+
return "world";
3+
}

‎test/testdata/sketch_simple/s_file.S

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}

‎test/testdata/sketch_simple/src/helper.h

Whitespace-only changes.

0 commit comments

Comments
 (0)
Please sign in to comment.