Skip to content

[breaking] Implementation of sketch profiles #1713

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

Merged
merged 35 commits into from
May 24, 2022
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
803782c
cosmetic: renamed import
cmaglie Apr 27, 2022
e82a46d
Simplify function pm.DownloadPlatformRelease
cmaglie Mar 31, 2022
1b1867f
Implementation of the Profiles parser
cmaglie Mar 14, 2022
da64af5
Added methods to get profiles from sketch
cmaglie Mar 14, 2022
670c9f1
Added gRPC parameters to support profiles
cmaglie Mar 14, 2022
79eb7f4
Added function to load packages for profiles
cmaglie Mar 14, 2022
183a8bb
Added support for profiles in compile command
cmaglie Apr 26, 2022
1b53f59
Added progress callback and installMissing flag (stubs) in pm.Prepare…
cmaglie Mar 31, 2022
4f8244a
Added auto-install procedures for profiles
cmaglie Mar 31, 2022
c21147b
Handle platform not found errors
cmaglie Mar 31, 2022
8527015
Draft implementation of upload with profiles
cmaglie Mar 28, 2022
10d95b2
Made packagemamager.loadToolsFromPackage public
cmaglie Apr 29, 2022
6cc5d65
Simplified callbacks in commands.Init
cmaglie Apr 29, 2022
18b0d34
cosmetic: added shortcut variable for library manager
cmaglie Apr 29, 2022
4a26bd4
cosmetic: added shortcut variable for package manager; small readabil…
cmaglie Apr 29, 2022
5449677
Wiring profiles into arduino-cli and gRPC implementation
cmaglie May 2, 2022
5972501
Made gRPC Init return a full Profile structure
cmaglie May 2, 2022
efa8899
(tech debt) Disable profiles if compiling with --libraries/--library
cmaglie May 2, 2022
61081f1
Fixed some linter warnings
cmaglie May 2, 2022
6156a26
Apply suggestions from code review
cmaglie May 5, 2022
9d1ec03
Added profiles specification docs
cmaglie May 10, 2022
d998f7f
Allow both sketch.yaml and .yml (with priority for .yaml)
cmaglie May 10, 2022
be8b325
Correctly handle nil return value
cmaglie May 10, 2022
614d652
Apply suggestions from code review
cmaglie May 19, 2022
8cdb8cb
Apply suggestions from code review
cmaglie May 19, 2022
bae72d0
Provide `core install` suggestions only when compiling without profiles
cmaglie May 19, 2022
170382d
Remove stray comment
cmaglie May 19, 2022
633022f
Fixed some comments in protoc files
cmaglie May 19, 2022
1fdbce6
Apply suggestions from code review
cmaglie May 19, 2022
b206d47
Apply suggestions from code review
cmaglie May 20, 2022
453f750
Implemented missing AsYaml methods and added tests
cmaglie May 20, 2022
75f4117
Apply suggestions from code review
cmaglie May 20, 2022
8203266
run of prettier formatter
cmaglie May 20, 2022
e20117f
Preserve profiles ordering in profiles.yaml
cmaglie May 20, 2022
21e8e86
Apply suggestions from code review
cmaglie May 20, 2022
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
15 changes: 15 additions & 0 deletions arduino/cores/cores.go
Original file line number Diff line number Diff line change
@@ -16,12 +16,16 @@
package cores

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"

"github.com/arduino/arduino-cli/arduino/resources"
"github.com/arduino/arduino-cli/arduino/utils"
"github.com/arduino/arduino-cli/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
paths "github.com/arduino/go-paths-helper"
@@ -122,6 +126,17 @@ func (dep *ToolDependency) String() string {
return dep.ToolPackager + ":" + dep.ToolName + "@" + dep.ToolVersion.String()
}

// InternalUniqueIdentifier returns the unique identifier for this object
func (dep *ToolDependency) InternalUniqueIdentifier(platformIndexURL *url.URL) string {
h := sha256.New()
h.Write([]byte(dep.String()))
if platformIndexURL != nil {
h.Write([]byte(platformIndexURL.String()))
}
res := dep.String() + "_" + hex.EncodeToString(h.Sum([]byte{}))[:16]
return utils.SanitizeName(res)
}

// DiscoveryDependencies is a list of DiscoveryDependency
type DiscoveryDependencies []*DiscoveryDependency

8 changes: 6 additions & 2 deletions arduino/cores/packagemanager/download.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ package packagemanager
import (
"fmt"

"github.com/arduino/arduino-cli/arduino"
"github.com/arduino/arduino-cli/arduino/cores"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"go.bug.st/downloader/v2"
@@ -128,6 +129,9 @@ func (pm *PackageManager) DownloadToolRelease(tool *cores.ToolRelease, config *d

// DownloadPlatformRelease downloads a PlatformRelease. If the platform is already downloaded a
// nil Downloader is returned.
func (pm *PackageManager) DownloadPlatformRelease(platform *cores.PlatformRelease, config *downloader.Config, label string, progressCB rpc.DownloadProgressCB) error {
return platform.Resource.Download(pm.DownloadDir, config, label, progressCB)
func (pm *PackageManager) DownloadPlatformRelease(platform *cores.PlatformRelease, config *downloader.Config, progressCB rpc.DownloadProgressCB) error {
if platform.Resource == nil {
return &arduino.PlatformNotFoundError{Platform: platform.String()}
}
return platform.Resource.Download(pm.DownloadDir, config, platform.String(), progressCB)
}
8 changes: 5 additions & 3 deletions arduino/cores/packagemanager/loader.go
Original file line number Diff line number Diff line change
@@ -128,7 +128,7 @@ func (pm *PackageManager) LoadHardwareFromDirectory(path *paths.Path) []error {
toolsSubdirPath := packagerPath.Join("tools")
if toolsSubdirPath.IsDir() {
pm.Log.Infof("Checking existence of 'tools' path: %s", toolsSubdirPath)
merr = append(merr, pm.loadToolsFromPackage(targetPackage, toolsSubdirPath)...)
merr = append(merr, pm.LoadToolsFromPackageDir(targetPackage, toolsSubdirPath)...)
}
// If the Package does not contain Platforms or Tools we remove it since does not contain anything valuable
if len(targetPackage.Platforms) == 0 && len(targetPackage.Tools) == 0 {
@@ -589,7 +589,9 @@ func convertUploadToolsToPluggableDiscovery(props *properties.Map) {
props.Merge(propsToAdd)
}

func (pm *PackageManager) loadToolsFromPackage(targetPackage *cores.Package, toolsPath *paths.Path) []error {
// LoadToolsFromPackageDir loads a set of tools from the given toolsPath. The tools will be loaded
// in the given *Package.
func (pm *PackageManager) LoadToolsFromPackageDir(targetPackage *cores.Package, toolsPath *paths.Path) []error {
pm.Log.Infof("Loading tools from dir: %s", toolsPath)

var merr []error
@@ -712,7 +714,7 @@ func (pm *PackageManager) LoadToolsFromBundleDirectory(toolsPath *paths.Path) er
} else {
// otherwise load the tools inside the unnamed package
unnamedPackage := pm.Packages.GetOrCreatePackage("")
pm.loadToolsFromPackage(unnamedPackage, toolsPath)
pm.LoadToolsFromPackageDir(unnamedPackage, toolsPath)
}
return nil
}
187 changes: 187 additions & 0 deletions arduino/cores/packagemanager/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// This file is part of arduino-cli.
//
// Copyright 2020-2022 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 packagemanager

import (
"fmt"
"net/url"

"github.com/arduino/arduino-cli/arduino"
"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/resources"
"github.com/arduino/arduino-cli/arduino/sketch"
"github.com/arduino/arduino-cli/cli/globals"
"github.com/arduino/arduino-cli/configuration"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
)

// LoadHardwareForProfile load the hardware platforms for the given profile.
// If installMissing is true then possibly missing tools and platforms will be downloaded and installed.
func (pm *PackageManager) LoadHardwareForProfile(p *sketch.Profile, installMissing bool, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) []error {
// Load required platforms
var merr []error
var platformReleases []*cores.PlatformRelease
indexURLs := map[string]*url.URL{}
for _, platformRef := range p.Platforms {
if platformRelease, err := pm.loadProfilePlatform(platformRef, installMissing, downloadCB, taskCB); err != nil {
merr = append(merr, fmt.Errorf("%s: %w", tr("loading required platform %s", platformRef), err))
logrus.WithField("platform", platformRef).WithError(err).Debugf("Error loading platform for profile")
} else {
platformReleases = append(platformReleases, platformRelease)
indexURLs[platformRelease.Platform.Name] = platformRef.PlatformIndexURL
logrus.WithField("platform", platformRef).Debugf("Loaded platform for profile")
}
}

// Load tools dependencies for the platforms
for _, platformRelease := range platformReleases {
// TODO: pm.FindPlatformReleaseDependencies(platformRelease)

for _, toolDep := range platformRelease.ToolDependencies {
indexURL := indexURLs[toolDep.ToolPackager]
if err := pm.loadProfileTool(toolDep, indexURL, installMissing, downloadCB, taskCB); err != nil {
merr = append(merr, fmt.Errorf("%s: %w", tr("loading required tool %s", toolDep), err))
logrus.WithField("tool", toolDep).WithField("index_url", indexURL).WithError(err).Debugf("Error loading tool for profile")
} else {
logrus.WithField("tool", toolDep).WithField("index_url", indexURL).Debugf("Loaded tool for profile")
}
}
}

return merr
}

func (pm *PackageManager) loadProfilePlatform(platformRef *sketch.ProfilePlatformReference, installMissing bool, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) (*cores.PlatformRelease, error) {
targetPackage := pm.Packages.GetOrCreatePackage(platformRef.Packager)
platform := targetPackage.GetOrCreatePlatform(platformRef.Architecture)
release := platform.GetOrCreateRelease(platformRef.Version)

uid := platformRef.InternalUniqueIdentifier()
destDir := configuration.ProfilesCacheDir(configuration.Settings).Join(uid)
if !destDir.IsDir() && installMissing {
// Try installing the missing platform
if err := pm.installMissingProfilePlatform(platformRef, destDir, downloadCB, taskCB); err != nil {
return nil, err
}
}
return release, pm.loadPlatformRelease(release, destDir)
}

func (pm *PackageManager) installMissingProfilePlatform(platformRef *sketch.ProfilePlatformReference, destDir *paths.Path, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) error {
// Instantiate a temporary package manager only for platform installation
_ = pm.TempDir.MkdirAll()
tmp, err := paths.MkTempDir(pm.TempDir.String(), "")
if err != nil {
return fmt.Errorf("installing missing platform: could not create temp dir %s", err)
}
tmpPm := NewPackageManager(tmp, tmp, pm.DownloadDir, tmp, pm.userAgent)
defer tmp.RemoveAll()

// Download the main index and parse it
taskCB(&rpc.TaskProgress{Name: tr("Downloading platform %s", platformRef)})
defaultIndexURL, _ := url.Parse(globals.DefaultIndexURL)
indexesToDownload := []*url.URL{defaultIndexURL}
if platformRef.PlatformIndexURL != nil {
indexesToDownload = append(indexesToDownload, platformRef.PlatformIndexURL)
}
for _, indexURL := range indexesToDownload {
if err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error downloading %s", indexURL)})
return &arduino.FailedDownloadError{Message: tr("Error downloading %s", indexURL), Cause: err}
}
indexResource := resources.IndexResource{URL: indexURL}
if err := indexResource.Download(tmpPm.IndexDir, downloadCB); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error downloading %s", indexURL)})
return &arduino.FailedDownloadError{Message: tr("Error downloading %s", indexURL), Cause: err}
}
if err := tmpPm.LoadPackageIndex(indexURL); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error loading index %s", indexURL)})
return &arduino.FailedInstallError{Message: tr("Error loading index %s", indexURL), Cause: err}
}
}

// Download the platform
tmpTargetPackage := tmpPm.Packages.GetOrCreatePackage(platformRef.Packager)
tmpPlatform := tmpTargetPackage.GetOrCreatePlatform(platformRef.Architecture)
tmpPlatformRelease := tmpPlatform.GetOrCreateRelease(platformRef.Version)
if err := tmpPm.DownloadPlatformRelease(tmpPlatformRelease, nil, downloadCB); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error downloading platform %s", tmpPlatformRelease)})
return &arduino.FailedInstallError{Message: tr("Error downloading platform %s", tmpPlatformRelease), Cause: err}
}
taskCB(&rpc.TaskProgress{Completed: true})

// Perform install
taskCB(&rpc.TaskProgress{Name: tr("Installing platform %s", tmpPlatformRelease)})
if err := tmpPm.InstallPlatformInDirectory(tmpPlatformRelease, destDir); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error installing platform %s", tmpPlatformRelease)})
return &arduino.FailedInstallError{Message: tr("Error installing platform %s", tmpPlatformRelease), Cause: err}
}
taskCB(&rpc.TaskProgress{Completed: true})
return nil
}

func (pm *PackageManager) loadProfileTool(toolRef *cores.ToolDependency, indexURL *url.URL, installMissing bool, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) error {
targetPackage := pm.Packages.GetOrCreatePackage(toolRef.ToolPackager)
tool := targetPackage.GetOrCreateTool(toolRef.ToolName)

uid := toolRef.InternalUniqueIdentifier(indexURL)
destDir := configuration.ProfilesCacheDir(configuration.Settings).Join(uid)

if !destDir.IsDir() && installMissing {
// Try installing the missing tool
toolRelease := tool.GetOrCreateRelease(toolRef.ToolVersion)
if toolRelease == nil {
return &arduino.InvalidVersionError{Cause: fmt.Errorf(tr("version %s not found", toolRef.ToolVersion))}
}
if err := pm.installMissingProfileTool(toolRelease, destDir, downloadCB, taskCB); err != nil {
return err
}
}

return pm.loadToolReleaseFromDirectory(tool, toolRef.ToolVersion, destDir)
}

func (pm *PackageManager) installMissingProfileTool(toolRelease *cores.ToolRelease, destDir *paths.Path, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB) error {
// Instantiate a temporary package manager only for platform installation
tmp, err := paths.MkTempDir(destDir.Parent().String(), "")
if err != nil {
return fmt.Errorf("installing missing platform: could not create temp dir %s", err)
}
defer tmp.RemoveAll()

// Download the tool
toolResource := toolRelease.GetCompatibleFlavour()
if toolResource == nil {
return &arduino.InvalidVersionError{Cause: fmt.Errorf(tr("version %s not available for this operating system", toolRelease))}
}
taskCB(&rpc.TaskProgress{Name: tr("Downloading tool %s", toolRelease)})
if err := toolResource.Download(pm.DownloadDir, nil, toolRelease.String(), downloadCB); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error downloading tool %s", toolRelease)})
return &arduino.FailedInstallError{Message: tr("Error installing tool %s", toolRelease), Cause: err}
}
taskCB(&rpc.TaskProgress{Completed: true})

// Install tool
taskCB(&rpc.TaskProgress{Name: tr("Installing tool %s", toolRelease)})
if err := toolResource.Install(pm.DownloadDir, tmp, destDir); err != nil {
taskCB(&rpc.TaskProgress{Name: tr("Error installing tool %s", toolRelease)})
return &arduino.FailedInstallError{Message: tr("Error installing tool %s", toolRelease), Cause: err}
}
taskCB(&rpc.TaskProgress{Completed: true})
return nil
}
37 changes: 37 additions & 0 deletions arduino/errors.go
Original file line number Diff line number Diff line change
@@ -193,6 +193,43 @@ func (e *UnknownFQBNError) ToRPCStatus() *status.Status {
return status.New(codes.NotFound, e.Error())
}

// UnknownProfileError is returned when the profile is not found
type UnknownProfileError struct {
Profile string
Cause error
}

func (e *UnknownProfileError) Error() string {
return composeErrorMsg(tr("Profile '%s' not found", e.Profile), e.Cause)
}

func (e *UnknownProfileError) Unwrap() error {
return e.Cause
}

// ToRPCStatus converts the error into a *status.Status
func (e *UnknownProfileError) ToRPCStatus() *status.Status {
return status.New(codes.NotFound, e.Error())
}

// InvalidProfileError is returned when the profile has errors
type InvalidProfileError struct {
Cause error
}

func (e *InvalidProfileError) Error() string {
return composeErrorMsg(tr("Invalid profile"), e.Cause)
}

func (e *InvalidProfileError) Unwrap() error {
return e.Cause
}

// ToRPCStatus converts the error into a *status.Status
func (e *InvalidProfileError) ToRPCStatus() *status.Status {
return status.New(codes.FailedPrecondition, e.Error())
}

// MissingPortAddressError is returned when the port protocol is mandatory and not specified
type MissingPortAddressError struct{}

252 changes: 252 additions & 0 deletions arduino/sketch/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// This file is part of arduino-cli.
//
// Copyright 2020-2022 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 license@arduino.cc.

package sketch

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"regexp"
"strings"

"github.com/arduino/arduino-cli/arduino/utils"
"github.com/arduino/go-paths-helper"
semver "go.bug.st/relaxed-semver"
"gopkg.in/yaml.v2"
)

// Project represents all the profiles defined for the sketch
type Project struct {
Profiles Profiles `yaml:"profiles"`
DefaultProfile string `yaml:"default_profile"`
}

// AsYaml outputs the sketch project file as YAML
func (p *Project) AsYaml() string {
res := "profiles:\n"
for _, profile := range p.Profiles {
res += fmt.Sprintf(" %s:\n", profile.Name)
res += profile.AsYaml()
res += "\n"
}
if p.DefaultProfile != "" {
res += fmt.Sprintf("default_profile: %s\n", p.DefaultProfile)
}
return res
}

// Profiles are a list of Profile
type Profiles []*Profile

// UnmarshalYAML decodes a Profiles section from YAML source.
func (p *Profiles) UnmarshalYAML(unmarshal func(interface{}) error) error {
unmarshaledProfiles := map[string]*Profile{}
if err := unmarshal(&unmarshaledProfiles); err != nil {
return err
}

var profilesData yaml.MapSlice
if err := unmarshal(&profilesData); err != nil {
return err
}

for _, profileData := range profilesData {
profileName, ok := profileData.Key.(string)
if !ok {
return fmt.Errorf("invalid profile name: %v", profileData.Key)
}
profile := unmarshaledProfiles[profileName]
profile.Name = profileName
*p = append(*p, profile)
}

return nil
}

// Profile is a sketch profile, it contains a reference to all the resources
// needed to build and upload a sketch
type Profile struct {
Name string
Notes string `yaml:"notes"`
FQBN string `yaml:"fqbn"`
Platforms ProfileRequiredPlatforms `yaml:"platforms"`
Libraries ProfileRequiredLibraries `yaml:"libraries"`
}

// AsYaml outputs the profile as Yaml
func (p *Profile) AsYaml() string {
res := ""
if p.Notes != "" {
res += fmt.Sprintf(" notes: %s\n", p.Notes)
}
res += fmt.Sprintf(" fqbn: %s\n", p.FQBN)
res += p.Platforms.AsYaml()
res += p.Libraries.AsYaml()
return res
}

// ProfileRequiredPlatforms is a list of ProfilePlatformReference (platforms
// required to build the sketch using this profile)
type ProfileRequiredPlatforms []*ProfilePlatformReference

// AsYaml outputs the required platforms as Yaml
func (p *ProfileRequiredPlatforms) AsYaml() string {
res := " platforms:\n"
for _, platform := range *p {
res += platform.AsYaml()
}
return res
}

// ProfileRequiredLibraries is a list of ProfileLibraryReference (libraries
// required to build the sketch using this profile)
type ProfileRequiredLibraries []*ProfileLibraryReference

// AsYaml outputs the required libraries as Yaml
func (p *ProfileRequiredLibraries) AsYaml() string {
res := " libraries:\n"
for _, lib := range *p {
res += lib.AsYaml()
}
return res
}

// ProfilePlatformReference is a reference to a platform
type ProfilePlatformReference struct {
Packager string
Architecture string
Version *semver.Version
PlatformIndexURL *url.URL
}

// InternalUniqueIdentifier returns the unique identifier for this object
func (p *ProfilePlatformReference) InternalUniqueIdentifier() string {
id := p.String()
h := sha256.Sum256([]byte(id))
res := fmt.Sprintf("%s:%s@%s_%s", p.Packager, p.Architecture, p.Version, hex.EncodeToString(h[:])[:16])
return utils.SanitizeName(res)
}

func (p *ProfilePlatformReference) String() string {
res := fmt.Sprintf("%s:%s@%s", p.Packager, p.Architecture, p.Version)
if p.PlatformIndexURL != nil {
res += fmt.Sprintf(" (%s)", p.PlatformIndexURL)
}
return res
}

// AsYaml outputs the platform reference as Yaml
func (p *ProfilePlatformReference) AsYaml() string {
res := fmt.Sprintf(" - platform: %s:%s (%s)\n", p.Packager, p.Architecture, p.Version)
if p.PlatformIndexURL != nil {
res += fmt.Sprintf(" platform_index_url: %s\n", p.PlatformIndexURL)
}
return res
}

func parseNameAndVersion(in string) (string, string, bool) {
re := regexp.MustCompile(`^([a-zA-Z0-9.\-_ :]+) \((.+)\)$`)
split := re.FindAllStringSubmatch(in, -1)
if len(split) != 1 || len(split[0]) != 3 {
return "", "", false
}
return split[0][1], split[0][2], true
}

// UnmarshalYAML decodes a ProfilePlatformReference from YAML source.
func (p *ProfilePlatformReference) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data map[string]string
if err := unmarshal(&data); err != nil {
return err
}
if platformID, ok := data["platform"]; !ok {
return fmt.Errorf(tr("missing '%s' directive", "platform"))
} else if platformID, platformVersion, ok := parseNameAndVersion(platformID); !ok {
return fmt.Errorf(tr("invalid '%s' directive", "platform"))
} else if c, err := semver.Parse(platformVersion); err != nil {
return fmt.Errorf("%s: %w", tr("error parsing version constraints"), err)
} else if split := strings.SplitN(platformID, ":", 2); len(split) != 2 {
return fmt.Errorf("%s: %s", tr("invalid platform identifier"), platformID)
} else {
p.Packager = split[0]
p.Architecture = split[1]
p.Version = c
}

if rawIndexURL, ok := data["platform_index_url"]; ok {
indexURL, err := url.Parse(rawIndexURL)
if err != nil {
return fmt.Errorf("%s: %w", tr("invalid platform index URL:"), err)
}
p.PlatformIndexURL = indexURL
}
return nil
}

// ProfileLibraryReference is a reference to a library
type ProfileLibraryReference struct {
Library string
Version *semver.Version
}

// UnmarshalYAML decodes a ProfileLibraryReference from YAML source.
func (l *ProfileLibraryReference) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data string
if err := unmarshal(&data); err != nil {
return err
}
if libName, libVersion, ok := parseNameAndVersion(data); !ok {
return fmt.Errorf("%s %s", tr("invalid library directive:"), data)
} else if v, err := semver.Parse(libVersion); err != nil {
return fmt.Errorf("%s %w", tr("invalid version:"), err)
} else {
l.Library = libName
l.Version = v
}
return nil
}

// AsYaml outputs the required library as Yaml
func (l *ProfileLibraryReference) AsYaml() string {
res := fmt.Sprintf(" - %s (%s)\n", l.Library, l.Version)
return res
}

func (l *ProfileLibraryReference) String() string {
return fmt.Sprintf("%s@%s", l.Library, l.Version)
}

// InternalUniqueIdentifier returns the unique identifier for this object
func (l *ProfileLibraryReference) InternalUniqueIdentifier() string {
id := l.String()
h := sha256.Sum256([]byte(id))
res := fmt.Sprintf("%s_%s", id, hex.EncodeToString(h[:])[:16])
return utils.SanitizeName(res)
}

// LoadProjectFile reads a sketch project file
func LoadProjectFile(file *paths.Path) (*Project, error) {
data, err := file.ReadFile()
if err != nil {
return nil, err
}
res := &Project{}
if err := yaml.Unmarshal(data, &res); err != nil {
return nil, err
}
return res, nil
}
34 changes: 34 additions & 0 deletions arduino/sketch/profiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This file is part of arduino-cli.
//
// Copyright 2020-2022 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 license@arduino.cc.

package sketch

import (
"fmt"
"testing"

"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
)

func TestProjectFileLoading(t *testing.T) {
sketchProj := paths.New("testdata", "SketchWithProfiles", "sketch.yml")
proj, err := LoadProjectFile(sketchProj)
require.NoError(t, err)
fmt.Println(proj)
golden, err := sketchProj.ReadFile()
require.NoError(t, err)
require.Equal(t, proj.AsYaml(), string(golden))
}
24 changes: 24 additions & 0 deletions arduino/sketch/sketch.go
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ type Sketch struct {
AdditionalFiles paths.PathList
RootFolderFiles paths.PathList // All files that are in the Sketch root
Metadata *Metadata
Project *Project
}

// Metadata is the kind of data associated to a project such as the connected board
@@ -93,6 +94,18 @@ func New(path *paths.Path) (*Sketch, error) {
Metadata: new(Metadata),
}

projectFile := path.Join("sketch.yaml")
if !projectFile.Exist() {
projectFile = path.Join("sketch.yml")
}
if projectFile.Exist() {
prj, err := LoadProjectFile(projectFile)
if err != nil {
return nil, fmt.Errorf("%s %w", tr("error loading sketch project file:"), err)
}
sketch.Project = prj
}

err := sketch.checkSketchCasing()
if e, ok := err.(*InvalidSketchFolderNameError); ok {
return nil, e
@@ -218,6 +231,17 @@ func (s *Sketch) ExportMetadata() error {
return nil
}

// GetProfile returns the requested profile or nil if the profile
// is not found.
func (s *Sketch) GetProfile(profileName string) *Profile {
for _, p := range s.Project.Profiles {
if p.Name == profileName {
return p
}
}
return nil
}

// checkSketchCasing returns an error if the casing of the sketch folder and the main file are different.
// Correct:
// MySketch/MySketch.ino
43 changes: 43 additions & 0 deletions arduino/sketch/testdata/SketchWithProfiles/sketch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
profiles:
nanorp:
fqbn: arduino:mbed_nano:nanorp2040connect
platforms:
- platform: arduino:mbed_nano (2.1.0)
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)

another_profile_name:
notes: testing the limit of the AVR platform, may be unstable
fqbn: arduino:avr:uno
platforms:
- platform: arduino:avr (1.8.4)
libraries:
- VitconMQTT (1.0.1)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)

tiny:
notes: testing the very limit of the AVR platform, it will be very unstable
fqbn: attiny:avr:ATtinyX5:cpu=attiny85,clock=internal16
platforms:
- platform: attiny:avr (1.0.2)
platform_index_url: http://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json
- platform: arduino:avr (1.8.3)
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)

feather:
fqbn: adafruit:samd:adafruit_feather_m0
platforms:
- platform: adafruit:samd (1.6.0)
platform_index_url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)

default_profile: nanorp
46 changes: 46 additions & 0 deletions cli/arguments/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// This file is part of arduino-cli.
//
// Copyright 2020-2022 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 license@arduino.cc.

package arguments

import "github.com/spf13/cobra"

// Profile contains the profile flag data.
// This is useful so all flags used by commands that need
// this information are consistent with each other.
type Profile struct {
profile string
}

// AddToCommand adds the flags used to set fqbn to the specified Command
func (f *Profile) AddToCommand(cmd *cobra.Command) {
cmd.Flags().StringVarP(&f.profile, "profile", "m", "", tr("Sketch profile to use"))
// TODO: register autocompletion
}

// Get returns the profile name
func (f *Profile) Get() string {
return f.profile
}

// String returns the profile name
func (f *Profile) String() string {
return f.profile
}

// Set sets the profile
func (f *Profile) Set(profile string) {
f.profile = profile
}
31 changes: 25 additions & 6 deletions cli/compile/compile.go
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ import (

var (
fqbnArg arguments.Fqbn // Fully Qualified Board Name, e.g.: arduino:avr:uno.
profileArg arguments.Profile // Profile to use
showProperties bool // Show all build preferences used instead of compiling.
preprocess bool // Print preprocessed code to stdout.
buildCachePath string // Builds of 'core.a' are saved into this path to be cached and reused.
@@ -91,6 +92,7 @@ func NewCommand() *cobra.Command {
}

fqbnArg.AddToCommand(compileCommand)
profileArg.AddToCommand(compileCommand)
compileCommand.Flags().BoolVar(&showProperties, "show-properties", false, tr("Show all build properties used instead of compiling."))
compileCommand.Flags().BoolVar(&preprocess, "preprocess", false, tr("Print preprocessed code to stdout instead of compiling."))
compileCommand.Flags().StringVar(&buildCachePath, "build-cache-path", "", tr("Builds of 'core.a' are saved into this path to be cached and reused."))
@@ -138,17 +140,32 @@ func NewCommand() *cobra.Command {
}

func runCompileCommand(cmd *cobra.Command, args []string) {
inst := instance.CreateAndInit()

logrus.Info("Executing `arduino-cli compile`")

if profileArg.Get() != "" {
if len(libraries) > 0 {
feedback.Errorf(tr("You cannot use the %s flag while compiling with a profile.", "--libraries"))
os.Exit(errorcodes.ErrBadArgument)
}
if len(library) > 0 {
feedback.Errorf(tr("You cannot use the %s flag while compiling with a profile.", "--library"))
os.Exit(errorcodes.ErrBadArgument)
}
}

path := ""
if len(args) > 0 {
path = args[0]
}

sketchPath := arguments.InitSketchPath(path)
sk := arguments.NewSketch(sketchPath)

inst, profile := instance.CreateAndInitWithProfile(profileArg.Get(), sketchPath)
if fqbnArg.String() == "" {
fqbnArg.Set(profile.GetFqbn())
}

fqbn, port := arguments.CalculateFQBNAndPort(&portArgs, &fqbnArg, inst, sk)

if keysKeychain != "" || signKey != "" || encryptKey != "" {
@@ -275,10 +292,12 @@ func runCompileCommand(cmd *cobra.Command, args []string) {
PlatformArchitecture: split[1],
})

if platform != nil {
feedback.Errorf(tr("Try running %s", fmt.Sprintf("`%s core install %s`", globals.VersionInfo.Application, platformErr.Platform)))
} else {
feedback.Errorf(tr("Platform %s is not found in any known index\nMaybe you need to add a 3rd party URL?", platformErr.Platform))
if profileArg.String() == "" {
if platform != nil {
feedback.Errorf(tr("Try running %s", fmt.Sprintf("`%s core install %s`", globals.VersionInfo.Application, platformErr.Platform)))
} else {
feedback.Errorf(tr("Platform %s is not found in any known index\nMaybe you need to add a 3rd party URL?", platformErr.Platform))
}
}
}
os.Exit(errorcodes.ErrGeneric)
41 changes: 33 additions & 8 deletions cli/instance/instance.go
Original file line number Diff line number Diff line change
@@ -37,15 +37,24 @@ var tr = i18n.Tr
// to execute further operations a valid Instance is mandatory.
// If Init returns errors they're printed only.
func CreateAndInit() *rpc.Instance {
inst, _ := CreateAndInitWithProfile("", nil)
return inst
}

// CreateAndInitWithProfile returns a new initialized instance using the given profile of the given sketch.
// If Create fails the CLI prints an error and exits since to execute further operations a valid Instance is mandatory.
// If Init returns errors they're printed only.
func CreateAndInitWithProfile(profileName string, sketchPath *paths.Path) (*rpc.Instance, *rpc.Profile) {
instance, err := Create()
if err != nil {
feedback.Errorf(tr("Error creating instance: %v"), err)
os.Exit(errorcodes.ErrGeneric)
}
for _, err := range Init(instance) {
profile, errs := InitWithProfile(instance, profileName, sketchPath)
for _, err := range errs {
feedback.Errorf(tr("Error initializing instance: %v"), err)
}
return instance
return instance, profile
}

// Create and return a new Instance.
@@ -63,19 +72,31 @@ func Create() (*rpc.Instance, error) {
// Package and library indexes files are automatically updated if the
// CLI is run for the first time.
func Init(instance *rpc.Instance) []error {
_, errs := InitWithProfile(instance, "", nil)
return errs
}

// InitWithProfile initializes instance by loading libraries and platforms specified in the given profile of the given sketch.
// In case of loading failures return a list of errors for each platform or library that we failed to load.
// Required Package and library indexes files are automatically downloaded.
func InitWithProfile(instance *rpc.Instance, profileName string, sketchPath *paths.Path) (*rpc.Profile, []error) {
errs := []error{}

// In case the CLI is executed for the first time
if err := FirstUpdate(instance); err != nil {
return append(errs, err)
return nil, append(errs, err)
}

downloadCallback := output.ProgressBar()
taskCallback := output.TaskProgress()

err := commands.Init(&rpc.InitRequest{
Instance: instance,
}, func(res *rpc.InitResponse) {
initReq := &rpc.InitRequest{Instance: instance}
if sketchPath != nil {
initReq.SketchPath = sketchPath.String()
initReq.Profile = profileName
}
var profile *rpc.Profile
err := commands.Init(initReq, func(res *rpc.InitResponse) {
if st := res.GetError(); st != nil {
errs = append(errs, errors.New(st.Message))
}
@@ -88,12 +109,16 @@ func Init(instance *rpc.Instance) []error {
taskCallback(progress.TaskProgress)
}
}

if p := res.GetProfile(); p != nil {
profile = p
}
})
if err != nil {
return append(errs, err)
errs = append(errs, err)
}

return errs
return profile, errs
}

// FirstUpdate downloads libraries and packages indexes if they don't exist.
9 changes: 8 additions & 1 deletion cli/upload/upload.go
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ import (
var (
fqbnArg arguments.Fqbn
portArgs arguments.Port
profileArg arguments.Profile
verbose bool
verify bool
importDir string
@@ -66,6 +67,7 @@ func NewCommand() *cobra.Command {

fqbnArg.AddToCommand(uploadCommand)
portArgs.AddToCommand(uploadCommand)
profileArg.AddToCommand(uploadCommand)
uploadCommand.Flags().StringVarP(&importDir, "input-dir", "", "", tr("Directory containing binaries to upload."))
uploadCommand.Flags().StringVarP(&importFile, "input-file", "i", "", tr("Binary file to upload."))
uploadCommand.Flags().BoolVarP(&verify, "verify", "t", false, tr("Verify uploaded binary after the upload."))
@@ -77,7 +79,6 @@ func NewCommand() *cobra.Command {
}

func runUploadCommand(command *cobra.Command, args []string) {
instance := instance.CreateAndInit()
logrus.Info("Executing `arduino-cli upload`")

path := ""
@@ -95,6 +96,12 @@ func runUploadCommand(command *cobra.Command, args []string) {
feedback.Errorf(tr("Error during Upload: %v"), err)
os.Exit(errorcodes.ErrGeneric)
}

instance, profile := instance.CreateAndInitWithProfile(profileArg.Get(), sketchPath)
if fqbnArg.String() == "" {
fqbnArg.Set(profile.GetFqbn())
}

fqbn, port := arguments.CalculateFQBNAndPort(&portArgs, &fqbnArg, instance, sk)

userFieldRes, err := upload.SupportedUserFields(context.Background(), &rpc.SupportedUserFieldsRequest{
14 changes: 11 additions & 3 deletions commands/compile/compile.go
Original file line number Diff line number Diff line change
@@ -91,6 +91,10 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
if pm == nil {
return nil, &arduino.InvalidInstanceError{}
}
lm := commands.GetLibraryManager(req.GetInstance().GetId())
if lm == nil {
return nil, &arduino.InvalidInstanceError{}
}

logrus.Tracef("Compile %s for %s started", req.GetSketchPath(), req.GetFqbn())
if req.GetSketchPath() == "" {
@@ -109,6 +113,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
if fqbnIn == "" {
return nil, &arduino.MissingFQBNError{}
}

fqbn, err := cores.ParseFQBN(fqbnIn)
if err != nil {
return nil, &arduino.InvalidFQBNError{Cause: err}
@@ -136,6 +141,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream

builderCtx := &types.Context{}
builderCtx.PackageManager = pm
builderCtx.LibrariesManager = lm
builderCtx.FQBN = fqbn
builderCtx.SketchLocation = sk.FullPath
builderCtx.ProgressCB = progressCB
@@ -144,11 +150,13 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
builderCtx.HardwareDirs = configuration.HardwareDirectories(configuration.Settings)
builderCtx.BuiltInToolsDirs = configuration.BundleToolsDirectories(configuration.Settings)

// FIXME: This will be redundant when arduino-builder will be part of the cli
builderCtx.OtherLibrariesDirs = paths.NewPathList(req.GetLibraries()...)
builderCtx.OtherLibrariesDirs.Add(configuration.LibrariesDir(configuration.Settings))

builderCtx.LibraryDirs = paths.NewPathList(req.Library...)

if len(builderCtx.OtherLibrariesDirs) > 0 || len(builderCtx.LibraryDirs) > 0 {
builderCtx.LibrariesManager = nil // let the builder rebuild the library manager
}
if req.GetBuildPath() == "" {
builderCtx.BuildPath = sk.BuildPath
} else {
@@ -188,6 +196,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
builderCtx.ArduinoAPIVersion = "10607"

// Check if Arduino IDE is installed and get it's libraries location.
// TODO: Remove?
dataDir := paths.New(configuration.Settings.GetString("directories.Data"))
preferencesTxt := dataDir.Join("preferences.txt")
ideProperties, err := properties.LoadFromPath(preferencesTxt)
@@ -210,7 +219,6 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
builderCtx.Stderr = errStream
builderCtx.Clean = req.GetClean()
builderCtx.OnlyUpdateCompilationDatabase = req.GetCreateCompilationDatabaseOnly()

builderCtx.SourceOverride = req.GetSourceOverride()

r = &rpc.CompileResponse{}
2 changes: 1 addition & 1 deletion commands/core/download.go
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ func downloadPlatform(pm *packagemanager.PackageManager, platformRelease *cores.
if err != nil {
return &arduino.FailedDownloadError{Message: tr("Error downloading platform %s", platformRelease), Cause: err}
}
return pm.DownloadPlatformRelease(platformRelease, config, platformRelease.String(), downloadCB)
return pm.DownloadPlatformRelease(platformRelease, config, downloadCB)
}

func downloadTool(pm *packagemanager.PackageManager, tool *cores.ToolRelease, downloadCB rpc.DownloadProgressCB) error {
285 changes: 171 additions & 114 deletions commands/instances.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions configuration/directories.go
Original file line number Diff line number Diff line change
@@ -90,3 +90,10 @@ func LibrariesDir(settings *viper.Viper) *paths.Path {
func PackagesDir(settings *viper.Viper) *paths.Path {
return paths.New(settings.GetString("directories.Data")).Join("packages")
}

// ProfilesCacheDir returns the full path to the profiles cache directory
// (it contains all the platforms and libraries used to compile a sketch
// using profiles)
func ProfilesCacheDir(settings *viper.Viper) *paths.Path {
return paths.New(settings.GetString("directories.Data")).Join("internal")
}
16 changes: 16 additions & 0 deletions docs/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,22 @@

Here you can find a list of migration guides to handle breaking changes between releases of the CLI.

## 0.23.0

### golang API: PackageManager.DownloadPlatformRelease no longer need `label` parameter

```go
func (pm *PackageManager) DownloadPlatformRelease(platform *cores.PlatformRelease, config *downloader.Config, label string, progressCB rpc.DownloadProgressCB) error {
```
is now:
```go
func (pm *PackageManager) DownloadPlatformRelease(platform *cores.PlatformRelease, config *downloader.Config, progressCB rpc.DownloadProgressCB) error {
```
Just remove the `label` parameter from legacy code.
## 0.22.0
### `github.com/arduino/arduino-cli/arduino.MultipleBoardsDetectedError` field changed type
117 changes: 117 additions & 0 deletions docs/sketch-project-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
Sketch metadata is defined in a file named `sketch.yaml`. This file is in YAML format.

## Build profiles

Arduino CLI provides support for reproducible builds through the use of build profiles.

A profile is a complete description of all the resources needed to build a sketch. The sketch project file may contain
multiple profiles.

Each profile will define:

- The board FQBN
- The target core platform name and version (with the 3rd party platform index URL if needed)
- A possible core platform name and version, that is a dependency of the target core platform (with the 3rd party
platform index URL if needed)
- The libraries used in the sketch (including their version)

The format of the file is the following:

```
profiles:
<PROFILE_NAME>:
notes: <USER_NOTES>
fqbn: <FQBN>
platforms:
- platform: <PLATFORM> (<PLATFORM_VERSION>)
platform_index_url: <3RD_PARTY_PLATFORM_URL>
- platform: <PLATFORM_DEPENDENCY> (<PLATFORM_DEPENDENCY_VERSION>)
platform_index_url: <3RD_PARTY_PLATFORM_DEPENDENCY_URL>
libraries:
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)
- <LIB_NAME> (<LIB_VERSION>)
...more profiles here...
```

There is an optional `profiles:` section containing all the profiles. Each field in a profile is mandatory (unless noted
otherwise below). The available fields are:

- `<PROFILE_NAME>` is the profile identifier, it’s a user-defined field, and the allowed characters are alphanumerics,
underscore `_`, dot `.`, and dash `-`.
- `<PLATFORM>` is the target core platform identifier, for example, `arduino:avr` or `adafruit:samd`.
- `<PLATFORM_VERSION>` is the target core platform version required.
- `<3RD_PARTY_PLATFORM_URL>` is the index URL to download the target core platform (also known as “Additional Boards
Manager URLs” in the Arduino IDE). This field can be omitted for the official `arduino:*` platforms.
- `<PLATFORM_DEPENDENCY>`, `<PLATFORM_DEPENDENCY_VERSION>`, and `<3RD_PARTY_PLATFORM_DEPENDENCY_URL>` contains the same
information as `<PLATFORM>`, `<PLATFORM_VERSION>`, and `<3RD_PARTY_PLATFORM_URL>` respectively but for the core
platform dependency of the main core platform. These fields are optional.
- `libraries:` is a section where the required libraries to build the project are defined. This section is optional.
- `<LIB_VERSION>` is the version required for the library, for example, `1.0.0`.
- `<USER_NOTES>` is a free text string available to the developer to add comments. This field is optional.

A complete example of a sketch project file may be the following:

```
profiles:
nanorp:
fqbn: arduino:mbed_nano:nanorp2040connect
platforms:
- platform: arduino:mbed_nano (2.1.0)
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)
another_profile_name:
notes: testing the limit of the AVR platform, may be unstable
fqbn: arduino:avr:uno
platforms:
- platform: arduino:avr (1.8.4)
libraries:
- VitconMQTT (1.0.1)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)
tiny:
notes: testing the very limit of the AVR platform, it will be very unstable
fqbn: attiny:avr:ATtinyX5:cpu=attiny85,clock=internal16
platforms:
- platform: attiny:avr@1.0.2
platform_index_url: https://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json
- platform: arduino:avr@1.8.3
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)
feather:
fqbn: adafruit:samd:adafruit_feather_m0
platforms:
- platform: adafruit:samd (1.6.0)
platform_index_url: https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
libraries:
- ArduinoIoTCloud (1.0.2)
- Arduino_ConnectionHandler (0.6.4)
- TinyDHT sensor library (1.1.0)
```

### Building a sketch

When a sketch project file is present, it can be leveraged to compile the sketch with the `--profile/-m` flag in the
`compile` command:

```
arduino-cli compile --profile nanorp
```

In this case, the sketch will be compiled using the core platform and libraries specified in the nanorp profile. If a
core platform or a library is missing it will be automatically downloaded and installed on the fly in an isolated
directory inside the data folder. The dedicated storage is not accessible to the user and is meant as a "cache" of the
resources used to build the sketch.

When using the profile-based build, the globally installed platforms and libraries are excluded from the compile and can
not be used in any way. In other words, the build is isolated from the system and will rely only on the resources
specified in the profile: this will ensure that the build is portable and reproducible independently from the platforms
and libraries installed in the system.
11 changes: 11 additions & 0 deletions docs/sketch-specification.md
Original file line number Diff line number Diff line change
@@ -66,6 +66,8 @@ the `data` folder, so any non-code files outside the `data` folder are stripped.

### Metadata

#### `sketch.json`

Arduino CLI and Arduino Web Editor use a file named sketch.json, located in the sketch root folder, to store sketch
metadata.

@@ -79,6 +81,15 @@ The `included_libs` key defines the library versions the Arduino Web Editor uses
Arduino Web Editor specific because all versions of all the Library Manager libraries are pre-installed in Arduino Web
Editor, while only one version of each library may be installed when using the other Arduino development software.

#### Sketch project file

This is an optional file named `sketch.yaml`, located in the root folder of the sketch.

Inside the sketch project file the user can define one or more "profiles": each profile is a description of all the
resources needed to build the sketch (platform and libraries each pinned to a specific version).

For more information see the [sketch project file](sketch-project-file.md) documentation.

### Secrets

Arduino Web Editor has a
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -107,6 +107,7 @@ nav:
- Integration options: integration-options.md
- sketch-build-process.md
- sketch-specification.md
- sketch-project-file.md
- library-specification.md
- platform-specification.md
- Pluggable discovery specification: pluggable-discovery-specification.md
1,250 changes: 647 additions & 603 deletions rpc/cc/arduino/cli/commands/v1/commands.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions rpc/cc/arduino/cli/commands/v1/commands.proto
Original file line number Diff line number Diff line change
@@ -198,6 +198,10 @@ message CreateResponse {
message InitRequest {
// An Arduino Core instance.
Instance instance = 1;
// Profile to use
string profile = 2;
// The path where the sketch is stored
string sketch_path = 3;
}

message InitResponse {
@@ -210,6 +214,8 @@ message InitResponse {
oneof message {
Progress init_progress = 1;
google.rpc.Status error = 2;
// Selected profile information
Profile profile = 3;
}
}

90 changes: 82 additions & 8 deletions rpc/cc/arduino/cli/commands/v1/common.pb.go
9 changes: 8 additions & 1 deletion rpc/cc/arduino/cli/commands/v1/common.proto
Original file line number Diff line number Diff line change
@@ -95,4 +95,11 @@ message Board {
// Fully qualified board name used to identify the board to machines. The FQBN
// is only available for installed boards.
string fqbn = 2;
}
}

message Profile {
// Name used to identify the profile within the sketch.
string name = 1;
// FQBN specified in the profile.
string fqbn = 2;
}