Skip to content

Commit af0b60e

Browse files
authored
gRPC: Compile will now fail if a platform core has been modified. (#2551)
* Slighlty moved variables near their utilization place * Added facility to keep timestamps of used files * Compile will fail if platforms are changed * Added integration test * Make linter happy * Fixed grammar * Test all monitored files
1 parent b987df1 commit af0b60e

File tree

8 files changed

+408
-132
lines changed

8 files changed

+408
-132
lines changed

Diff for: commands/cmderrors/cmderrors.go

+16
Original file line numberDiff line numberDiff line change
@@ -869,3 +869,19 @@ func (e *MultipleLibraryInstallDetected) Error() string {
869869
func (e *MultipleLibraryInstallDetected) ToRPCStatus() *status.Status {
870870
return status.New(codes.InvalidArgument, e.Error())
871871
}
872+
873+
// InstanceNeedsReinitialization
874+
type InstanceNeedsReinitialization struct {
875+
}
876+
877+
func (e *InstanceNeedsReinitialization) Error() string {
878+
return tr("The instance is no longer valid and needs to be reinitialized")
879+
}
880+
881+
// ToRPCStatus converts the error into a *status.Status
882+
func (e *InstanceNeedsReinitialization) ToRPCStatus() *status.Status {
883+
st, _ := status.
884+
New(codes.InvalidArgument, e.Error()).
885+
WithDetails(&rpc.InstanceNeedsReinitializationError{})
886+
return st
887+
}

Diff for: commands/compile/compile.go

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
6363
}
6464
defer release()
6565

66+
if pme.Dirty() {
67+
return nil, &cmderrors.InstanceNeedsReinitialization{}
68+
}
69+
6670
lm, err := instances.GetLibraryManager(req.GetInstance())
6771
if err != nil {
6872
return nil, err

Diff for: internal/arduino/cores/cores.go

+48
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"path/filepath"
2525
"sort"
2626
"strings"
27+
"time"
2728

2829
"github.com/arduino/arduino-cli/internal/arduino/globals"
2930
"github.com/arduino/arduino-cli/internal/arduino/resources"
@@ -70,13 +71,59 @@ type PlatformRelease struct {
7071
Programmers map[string]*Programmer `json:"-"`
7172
Menus *properties.Map `json:"-"`
7273
InstallDir *paths.Path `json:"-"`
74+
Timestamps *TimestampsStore // Contains the timestamps of the files used to build this PlatformRelease
7375
IsTrusted bool `json:"-"`
7476
PluggableDiscoveryAware bool `json:"-"` // true if the Platform supports pluggable discovery (no compatibility layer required)
7577
Monitors map[string]*MonitorDependency `json:"-"`
7678
MonitorsDevRecipes map[string]string `json:"-"`
7779
Compatible bool `json:"-"` // true if at all ToolDependencies are available for the current OS/ARCH.
7880
}
7981

82+
// TimestampsStore is a generic structure to store timestamps
83+
type TimestampsStore struct {
84+
timestamps map[*paths.Path]*time.Time
85+
}
86+
87+
// NewTimestampsStore creates a new TimestampsStore
88+
func NewTimestampsStore() *TimestampsStore {
89+
return &TimestampsStore{
90+
timestamps: map[*paths.Path]*time.Time{},
91+
}
92+
}
93+
94+
// AddFile adds a file to the TimestampsStore
95+
func (t *TimestampsStore) AddFile(path *paths.Path) {
96+
if info, err := path.Stat(); err != nil {
97+
t.timestamps[path] = nil // Save a missing file with a nil timestamp
98+
} else {
99+
modtime := info.ModTime()
100+
t.timestamps[path] = &modtime
101+
}
102+
}
103+
104+
// Dirty returns true if one of the files stored in the TimestampsStore has been
105+
// changed after being added to the store.
106+
func (t *TimestampsStore) Dirty() bool {
107+
for path, timestamp := range t.timestamps {
108+
if info, err := path.Stat(); err != nil {
109+
if timestamp != nil {
110+
return true
111+
}
112+
} else {
113+
if timestamp == nil || info.ModTime() != *timestamp {
114+
return true
115+
}
116+
}
117+
}
118+
return false
119+
}
120+
121+
// Dirty returns true if one of the files of this PlatformRelease has been changed
122+
// (it means that the PlatformRelease should be rebuilt to be used correctly).
123+
func (release *PlatformRelease) Dirty() bool {
124+
return release.Timestamps.Dirty()
125+
}
126+
80127
// BoardManifest contains information about a board. These metadata are usually
81128
// provided by the package_index.json
82129
type BoardManifest struct {
@@ -207,6 +254,7 @@ func (platform *Platform) GetOrCreateRelease(version *semver.Version) *PlatformR
207254
Properties: properties.NewMap(),
208255
Programmers: map[string]*Programmer{},
209256
Platform: platform,
257+
Timestamps: NewTimestampsStore(),
210258
}
211259
platform.Releases[tag] = release
212260
return release

Diff for: internal/arduino/cores/packagemanager/loader.go

+17-10
Original file line numberDiff line numberDiff line change
@@ -239,29 +239,32 @@ func (pm *Builder) loadPlatform(targetPackage *cores.Package, architecture strin
239239
func (pm *Builder) loadPlatformRelease(platform *cores.PlatformRelease, path *paths.Path) error {
240240
platform.InstallDir = path
241241

242-
// Some useful paths
243-
installedJSONPath := path.Join("installed.json")
244-
platformTxtPath := path.Join("platform.txt")
245-
platformTxtLocalPath := path.Join("platform.local.txt")
246-
programmersTxtPath := path.Join("programmers.txt")
247-
248242
// If the installed.json file is found load it, this is done to handle the
249243
// case in which the platform's index and its url have been deleted locally,
250244
// if we don't load it some information about the platform is lost
245+
installedJSONPath := path.Join("installed.json")
246+
platform.Timestamps.AddFile(installedJSONPath)
251247
if installedJSONPath.Exist() {
252248
if _, err := pm.LoadPackageIndexFromFile(installedJSONPath); err != nil {
253249
return fmt.Errorf(tr("loading %[1]s: %[2]s"), installedJSONPath, err)
254250
}
255251
}
256252

253+
// TODO: why CLONE?
254+
platform.Properties = platform.Properties.Clone()
255+
257256
// Create platform properties
258-
platform.Properties = platform.Properties.Clone() // TODO: why CLONE?
259-
if p, err := properties.SafeLoad(platformTxtPath.String()); err == nil {
257+
platformTxtPath := path.Join("platform.txt")
258+
platform.Timestamps.AddFile(platformTxtPath)
259+
if p, err := properties.SafeLoadFromPath(platformTxtPath); err == nil {
260260
platform.Properties.Merge(p)
261261
} else {
262262
return fmt.Errorf(tr("loading %[1]s: %[2]s"), platformTxtPath, err)
263263
}
264-
if p, err := properties.SafeLoad(platformTxtLocalPath.String()); err == nil {
264+
265+
platformTxtLocalPath := path.Join("platform.local.txt")
266+
platform.Timestamps.AddFile(platformTxtLocalPath)
267+
if p, err := properties.SafeLoadFromPath(platformTxtLocalPath); err == nil {
265268
platform.Properties.Merge(p)
266269
} else {
267270
return fmt.Errorf(tr("loading %[1]s: %[2]s"), platformTxtLocalPath, err)
@@ -287,7 +290,9 @@ func (pm *Builder) loadPlatformRelease(platform *cores.PlatformRelease, path *pa
287290
}
288291

289292
// Create programmers properties
290-
if programmersProperties, err := properties.SafeLoad(programmersTxtPath.String()); err == nil {
293+
programmersTxtPath := path.Join("programmers.txt")
294+
platform.Timestamps.AddFile(programmersTxtPath)
295+
if programmersProperties, err := properties.SafeLoadFromPath(programmersTxtPath); err == nil {
291296
for programmerID, programmerProps := range programmersProperties.FirstLevelOf() {
292297
if !platform.PluggableDiscoveryAware {
293298
convertUploadToolsToPluggableDiscovery(programmerProps)
@@ -410,12 +415,14 @@ func (pm *Builder) loadBoards(platform *cores.PlatformRelease) error {
410415
}
411416

412417
boardsTxtPath := platform.InstallDir.Join("boards.txt")
418+
platform.Timestamps.AddFile(boardsTxtPath)
413419
allBoardsProperties, err := properties.LoadFromPath(boardsTxtPath)
414420
if err != nil {
415421
return err
416422
}
417423

418424
boardsLocalTxtPath := platform.InstallDir.Join("boards.local.txt")
425+
platform.Timestamps.AddFile(boardsLocalTxtPath)
419426
if boardsLocalProperties, err := properties.SafeLoadFromPath(boardsLocalTxtPath); err == nil {
420427
allBoardsProperties.Merge(boardsLocalProperties)
421428
} else {

Diff for: internal/arduino/cores/packagemanager/package_manager.go

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"path"
2424
"path/filepath"
25+
"slices"
2526
"strconv"
2627
"strings"
2728
"sync"
@@ -898,3 +899,11 @@ func (pme *Explorer) NormalizeFQBN(fqbn *cores.FQBN) (*cores.FQBN, error) {
898899
}
899900
return normalizedFqbn, nil
900901
}
902+
903+
// Dirty returns true if one of the loaded platforms needs to be re-initialized
904+
// due to file changes in the platform releases.
905+
func (pme *Explorer) Dirty() bool {
906+
return slices.ContainsFunc(
907+
pme.InstalledPlatformReleases(),
908+
(*cores.PlatformRelease).Dirty)
909+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2022 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package daemon_test
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
"io"
23+
"testing"
24+
"time"
25+
26+
"github.com/arduino/arduino-cli/internal/integrationtest"
27+
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
28+
"github.com/arduino/go-paths-helper"
29+
30+
"github.com/stretchr/testify/require"
31+
)
32+
33+
func TestDetectionOfChangesInCoreBeforeCompile(t *testing.T) {
34+
// See: https://github.com/arduino/arduino-cli/issues/2523
35+
36+
env, cli := integrationtest.CreateEnvForDaemon(t)
37+
defer env.CleanUp()
38+
39+
// Create a new instance of the daemon
40+
grpcInst := cli.Create()
41+
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
42+
fmt.Printf("INIT> %v\n", ir.GetMessage())
43+
}))
44+
45+
// Install avr core
46+
installCl, err := grpcInst.PlatformInstall(context.Background(), "arduino", "avr", "", true)
47+
require.NoError(t, err)
48+
for {
49+
installResp, err := installCl.Recv()
50+
if errors.Is(err, io.EOF) {
51+
break
52+
}
53+
require.NoError(t, err)
54+
fmt.Printf("INSTALL> %v\n", installResp)
55+
}
56+
installCl.CloseSend()
57+
58+
// Utility functions: tryCompile
59+
sketchPath, err := paths.New("testdata", "bare_minimum").Abs()
60+
require.NoError(t, err)
61+
tryCompile := func() error {
62+
compileCl, err := grpcInst.Compile(context.Background(), "arduino:avr:uno", sketchPath.String(), "")
63+
require.NoError(t, err)
64+
defer compileCl.CloseSend()
65+
for {
66+
if compileResp, err := compileCl.Recv(); errors.Is(err, io.EOF) {
67+
return nil
68+
} else if err != nil {
69+
return err
70+
} else {
71+
fmt.Printf("COMPILE> %v\n", compileResp)
72+
}
73+
}
74+
}
75+
76+
// Utility functions: tryTouch will touch a file and see if the compile detects the change
77+
tryTouch := func(fileToTouch *paths.Path) {
78+
time.Sleep(time.Second) // await at least one second so the timestamp of the file is different
79+
80+
// touch the file
81+
f, err := fileToTouch.Append()
82+
require.NoError(t, err)
83+
_, err = f.WriteString("\n")
84+
require.NoError(t, err)
85+
require.NoError(t, f.Close())
86+
87+
// try compile: should fail
88+
err = tryCompile()
89+
require.Error(t, err)
90+
require.Contains(t, err.Error(), "The instance is no longer valid and needs to be reinitialized")
91+
92+
// re-init instance
93+
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
94+
fmt.Printf("INIT> %v\n", ir.GetMessage())
95+
}))
96+
97+
// try compile: should succeed
98+
require.NoError(t, tryCompile())
99+
}
100+
101+
avrCorePath := cli.DataDir().Join("packages", "arduino", "hardware", "avr", "1.8.6")
102+
tryTouch(avrCorePath.Join("installed.json"))
103+
tryTouch(avrCorePath.Join("platform.txt"))
104+
tryTouch(avrCorePath.Join("platform.local.txt"))
105+
tryTouch(avrCorePath.Join("programmers.txt"))
106+
tryTouch(avrCorePath.Join("boards.txt"))
107+
tryTouch(avrCorePath.Join("boards.local.txt"))
108+
109+
// Delete a file and check if the change is detected
110+
require.NoError(t, avrCorePath.Join("programmers.txt").Remove())
111+
err = tryCompile()
112+
require.Error(t, err)
113+
require.Contains(t, err.Error(), "The instance is no longer valid and needs to be reinitialized")
114+
115+
// Re-init instance and check again
116+
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
117+
fmt.Printf("INIT> %v\n", ir.GetMessage())
118+
}))
119+
require.NoError(t, tryCompile())
120+
121+
// Create a file and check if the change is detected
122+
{
123+
f, err := avrCorePath.Join("programmers.txt").Create()
124+
require.NoError(t, err)
125+
require.NoError(t, f.Close())
126+
}
127+
err = tryCompile()
128+
require.Error(t, err)
129+
require.Contains(t, err.Error(), "The instance is no longer valid and needs to be reinitialized")
130+
131+
// Re-init instance and check again
132+
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
133+
fmt.Printf("INIT> %v\n", ir.GetMessage())
134+
}))
135+
require.NoError(t, tryCompile())
136+
}

0 commit comments

Comments
 (0)