Skip to content

Allow platforms without fixed version in profiles. #2940

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 4 commits into from
Jul 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions commands/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ func (s *arduinoCoreServerImpl) Init(req *rpc.InitRequest, stream rpc.ArduinoCor
s := &cmderrors.PlatformLoadingError{Cause: err}
responseError(s.GRPCStatus())
}
} else if profile.RequireSystemInstalledPlatform() {
for _, err := range pmb.LoadGlobalHardwareForProfile(profile) {
s := &cmderrors.PlatformLoadingError{Cause: err}
responseError(s.GRPCStatus())
}
} else {
// Load platforms from profile
errs := pmb.LoadHardwareForProfile(ctx, profile, true, downloadCallback, taskCallback, s.settings)
Expand Down
12 changes: 10 additions & 2 deletions docs/sketch-project-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ profiles:
fqbn: <FQBN>
programmer: <PROGRAMMER>
platforms:
- platform: <PLATFORM> (<PLATFORM_VERSION>)
- platform: <PLATFORM> [(<PLATFORM_VERSION>)]
platform_index_url: <3RD_PARTY_PLATFORM_URL>
- platform: <PLATFORM_DEPENDENCY> (<PLATFORM_DEPENDENCY_VERSION>)
- platform: <PLATFORM_DEPENDENCY> [(<PLATFORM_DEPENDENCY_VERSION>)]
platform_index_url: <3RD_PARTY_PLATFORM_DEPENDENCY_URL>
libraries:
- <INDEX_LIB_NAME> (<INDEX_LIB_VERSION>)
Expand Down Expand Up @@ -73,6 +73,14 @@ The following fields are available since Arduino CLI 1.1.0:
`baudrate: 115200`) but any setting/value can be specified. Multiple settings can be set. These fields are optional.
- `<PORT_PROTOCOL>` is the protocol for the port used to upload and monitor the board. This field is optional.

#### Using a system-installed platform.

The fields `<PLATFORM_VERSION>` and `<PLATFORM_DEPENDENCY_VERSION>` are optional, if they are omitted, the sketch
compilation will use the platforms installed system-wide. This could be helpful during the development of a platform
(where a specific release is not yet available), or if a specific version of a platform is not a strict requirement.

#### An example of a complete project file.

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

```
Expand Down
7 changes: 7 additions & 0 deletions internal/arduino/cores/packagemanager/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ import (
"github.com/sirupsen/logrus"
)

// LoadGlobalHardwareForProfile loads the hardware platforms for the given profile.
// It uses the global package manager and does not download or install any missing tools or platforms.
func (pmb *Builder) LoadGlobalHardwareForProfile(p *sketch.Profile) []error {
pmb.profile = p
return pmb.LoadHardware()
}

// 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 (pmb *Builder) LoadHardwareForProfile(ctx context.Context, p *sketch.Profile, installMissing bool, downloadCB rpc.DownloadProgressCB, taskCB rpc.TaskProgressCB, settings *configuration.Settings) []error {
Expand Down
78 changes: 65 additions & 13 deletions internal/arduino/sketch/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ type Profile struct {
Libraries ProfileRequiredLibraries `yaml:"libraries"`
}

// UsesSystemPlatform checks if this profile requires a system installed platform.
func (p *Profile) RequireSystemInstalledPlatform() bool {
return p.Platforms[0].RequireSystemInstalledPlatform()
}

// ToRpc converts this Profile to an rpc.SketchProfile
func (p *Profile) ToRpc() *rpc.SketchProfile {
var portConfig *rpc.MonitorPortConfiguration
Expand Down Expand Up @@ -182,6 +187,20 @@ func (p *ProfileRequiredPlatforms) AsYaml() string {
return res
}

func (p *ProfileRequiredPlatforms) UnmarshalYAML(unmarshal func(interface{}) error) error {
_p := (*[]*ProfilePlatformReference)(p)
if err := unmarshal(_p); err != nil {
return err
}
requireSystemPlatform := (*_p)[0].RequireSystemInstalledPlatform()
for _, platform := range *_p {
if platform.RequireSystemInstalledPlatform() != requireSystemPlatform {
return errors.New(i18n.Tr("all platforms in a profile must either require a specific version or not"))
}
}
return nil
}

// ProfileRequiredLibraries is a list of ProfileLibraryReference (libraries
// required to build the sketch using this profile)
type ProfileRequiredLibraries []*ProfileLibraryReference
Expand All @@ -206,6 +225,12 @@ type ProfilePlatformReference struct {
PlatformIndexURL *url.URL
}

// RequireSystemInstalledPlatform returns true if the platform reference
// does not specify a version, meaning it requires the system installed platform.
func (p *ProfilePlatformReference) RequireSystemInstalledPlatform() bool {
return p.Version == nil
}

// InternalUniqueIdentifier returns the unique identifier for this object
func (p *ProfilePlatformReference) InternalUniqueIdentifier() string {
id := p.String()
Expand All @@ -224,20 +249,38 @@ func (p *ProfilePlatformReference) String() string {

// 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)
res := ""
if p.Version != nil {
res += fmt.Sprintf(" - platform: %s:%s (%s)\n", p.Packager, p.Architecture, p.Version)
} else {
res += fmt.Sprintf(" - platform: %s:%s\n", p.Packager, p.Architecture)
}
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
{
// Try to parse the input string in the format "VENDOR:ARCH (VERSION)"
re := regexp.MustCompile(`^([a-zA-Z0-9.\-_ :]+) \((.+)\)$`)
split := re.FindAllStringSubmatch(in, -1)
if len(split) == 1 && len(split[0]) == 3 {
return split[0][1], split[0][2], true
}
}

{
// Try to parse the input string in the format "VENDOR:ARCH"
re := regexp.MustCompile(`^([a-zA-Z0-9.\-_ :]+)$`)
split := re.FindAllStringSubmatch(in, -1)
if len(split) == 1 && len(split[0]) == 2 {
return split[0][1], "", true
}
}
return split[0][1], split[0][2], true

return "", "", false
}

// UnmarshalYAML decodes a ProfilePlatformReference from YAML source.
Expand All @@ -250,14 +293,23 @@ func (p *ProfilePlatformReference) UnmarshalYAML(unmarshal func(interface{}) err
return errors.New(i18n.Tr("missing '%s' directive", "platform"))
} else if platformID, platformVersion, ok := parseNameAndVersion(platformID); !ok {
return errors.New(i18n.Tr("invalid '%s' directive", "platform"))
} else if c, err := semver.Parse(platformVersion); err != nil {
return fmt.Errorf("%s: %w", i18n.Tr("error parsing version constraints"), err)
} else if split := strings.SplitN(platformID, ":", 2); len(split) != 2 {
return fmt.Errorf("%s: %s", i18n.Tr("invalid platform identifier"), platformID)
} else {
p.Packager = split[0]
p.Architecture = split[1]
p.Version = c
var version *semver.Version
if platformVersion != "" {
if v, err := semver.Parse(platformVersion); err != nil {
return fmt.Errorf("%s: %w", i18n.Tr("error parsing version constraints"), err)
} else {
version = v
}
}

if split := strings.SplitN(platformID, ":", 2); len(split) != 2 {
return fmt.Errorf("%s: %s", i18n.Tr("invalid platform identifier"), platformID)
} else {
p.Packager = split[0]
p.Architecture = split[1]
p.Version = version
}
}

if rawIndexURL, ok := data["platform_index_url"]; ok {
Expand Down
13 changes: 13 additions & 0 deletions internal/arduino/sketch/profiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,17 @@ func TestProjectFileLoading(t *testing.T) {
require.NoError(t, err)
require.Equal(t, proj.AsYaml(), string(golden))
}
{
sketchProj := paths.New("testdata", "profiles", "profile_1.yml")
proj, err := LoadProjectFile(sketchProj)
require.NoError(t, err)
golden, err := sketchProj.ReadFile()
require.NoError(t, err)
require.Equal(t, string(golden), proj.AsYaml())
}
{
sketchProj := paths.New("testdata", "profiles", "bad_profile_1.yml")
_, err := LoadProjectFile(sketchProj)
require.Error(t, err)
}
}
9 changes: 9 additions & 0 deletions internal/arduino/sketch/testdata/profiles/bad_profile_1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
profiles:
tiny:
notes: Invalid profile mixing versioned and non-versioned platforms.
fqbn: attiny:avr:ATtinyX5:cpu=attiny85,clock=internal16
platforms:
- platform: attiny:avr
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)

12 changes: 12 additions & 0 deletions internal/arduino/sketch/testdata/profiles/profile_1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
profiles:
giga:
fqbn: arduino:mbed_giga:giga
platforms:
- platform: arduino:mbed_giga (4.3.1)

giga_any:
fqbn: arduino:mbed_giga:giga
platforms:
- platform: arduino:mbed_giga

default_profile: giga_any