From d6d16532c1a6eb4160b8cf8d3b4075e74d23f782 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Thu, 8 Jul 2021 12:11:35 +0200
Subject: [PATCH 01/10] Refactor codebase to use a single Sketch struct

---
 arduino/builder/builder.go                    |  46 ---
 arduino/builder/builder_test.go               |  59 ---
 arduino/builder/sketch.go                     | 234 ++----------
 arduino/builder/sketch_test.go                | 256 ++-----------
 .../testdata/TestMergeSketchSources.txt       |   6 +-
 .../testdata/TestMergeSketchSources_win.txt   |   6 +-
 arduino/libraries/loader.go                   |   4 +-
 arduino/sketch/sketch.go                      | 297 ++++++++++------
 arduino/sketch/sketch_test.go                 | 336 ++++++++++++------
 .../SketchBothInoAndPde.ino}                  |   0
 .../SketchBothInoAndPde.pde}                  |   0
 .../SketchMultipleMainFiles.ino               |   0
 .../SketchMultipleMainFiles.pde               |   0
 .../testdata/SketchPde/SketchPde.pde          |   0
 .../SketchSimple.ino}                         |   0
 .../{TestNew => SketchSimple}/other.cpp       |   0
 .../testdata/SketchSymlinkSrc/.#sketch.ino    |   2 +
 .../SketchSymlinkSrc/SketchSymlinkSrc.ino     |   7 +
 .../sketch/testdata/SketchSymlinkSrc/doc.txt  |   0
 .../sketch/testdata/SketchSymlinkSrc/header.h |   1 +
 .../sketch/testdata/SketchSymlinkSrc/old.pde  |   0
 .../testdata/SketchSymlinkSrc/other.ino       |   3 +
 .../sketch/testdata/SketchSymlinkSrc/s_file.S |   0
 .../SketchSymlinkSrc/src/dont_load_me.ino     |   2 +
 .../testdata/SketchSymlinkSrc/src/helper.h    |   0
 .../testdata/SketchWithWrongMain/main.ino}    |   1 -
 arduino/sketches/sketches.go                  | 148 --------
 arduino/sketches/sketches_test.go             | 132 -------
 cli/compile/compile.go                        |   4 +-
 cli/sketch/archive.go                         |  14 +-
 cli/upload/upload.go                          |   4 +-
 commands/board/attach.go                      |  12 +-
 commands/compile/compile.go                   |  16 +-
 commands/debug/debug_info.go                  |  18 +-
 commands/instances.go                         |  29 +-
 commands/sketch/archive.go                    |   8 +-
 commands/upload/upload.go                     |  25 +-
 commands/upload/upload_test.go                |  11 +-
 legacy/builder/builder.go                     |  10 +-
 legacy/builder/container_add_prototypes.go    |   4 +-
 legacy/builder/container_find_includes.go     |   2 +-
 .../container_merge_copy_sketch_files.go      |  10 +-
 legacy/builder/container_setup.go             |  13 +-
 legacy/builder/create_cmake_rule.go           |   2 +-
 legacy/builder/ctags_runner.go                |   2 +-
 legacy/builder/filter_sketch_source.go        |   6 +-
 .../builder/merge_sketch_with_bootloader.go   |   2 +-
 legacy/builder/preprocess_sketch.go           |   4 +-
 legacy/builder/setup_build_properties.go      |   2 +-
 legacy/builder/sketch_loader.go               |  68 +---
 .../test/Baladuino/Baladuino.preprocessed.txt |   8 +-
 ...harWithEscapedDoubleQuote.preprocessed.txt |  46 +--
 ...deBetweenMultilineComment.preprocessed.txt |   8 +-
 .../LineContinuations.preprocessed.txt        |   8 +-
 .../SketchWithIfDef.preprocessed.txt          |  14 +-
 .../SketchWithIfDef.resolved.directives.txt   |   2 +-
 .../SketchWithStruct.preprocessed.txt         |  10 +-
 .../StringWithComment.preprocessed.txt        |   8 +-
 legacy/builder/test/ctags_runner_test.go      |  47 ---
 legacy/builder/test/sketch1/merged_sketch.txt |   2 +-
 .../sketch_with_config.preprocessed.txt       |   8 +-
 .../sketch.preprocessed.SAM.txt               |  14 +-
 .../sketch_with_ifdef/sketch.preprocessed.txt |  14 +-
 .../sketch_with_templates_and_shift.cpp       |  19 -
 legacy/builder/types/context.go               |   9 +-
 legacy/builder/types/types.go                 |  54 +--
 test/test_compile.py                          |  10 +-
 67 files changed, 700 insertions(+), 1387 deletions(-)
 delete mode 100644 arduino/builder/builder.go
 delete mode 100644 arduino/builder/builder_test.go
 rename arduino/{sketches/testdata/SketchCasingCorrect/SketchCasingCorrect.ino => sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.ino} (100%)
 rename arduino/{sketches/testdata/SketchCasingWrong/sketchcasingwrong.ino => sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.pde} (100%)
 rename arduino/{sketches => sketch}/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.ino (100%)
 rename arduino/{sketches => sketch}/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.pde (100%)
 rename arduino/{sketches => sketch}/testdata/SketchPde/SketchPde.pde (100%)
 rename arduino/sketch/testdata/{TestNew/TestNew.ino => SketchSimple/SketchSimple.ino} (100%)
 rename arduino/sketch/testdata/{TestNew => SketchSimple}/other.cpp (100%)
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/.#sketch.ino
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/SketchSymlinkSrc.ino
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/doc.txt
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/header.h
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/old.pde
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/other.ino
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/s_file.S
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/src/dont_load_me.ino
 create mode 100644 arduino/sketch/testdata/SketchSymlinkSrc/src/helper.h
 rename arduino/{sketches/testdata/Sketch1/Sketch1.ino => sketch/testdata/SketchWithWrongMain/main.ino} (96%)
 delete mode 100644 arduino/sketches/sketches.go
 delete mode 100644 arduino/sketches/sketches_test.go
 delete mode 100644 legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.cpp

diff --git a/arduino/builder/builder.go b/arduino/builder/builder.go
deleted file mode 100644
index a0c3d9b8153..00000000000
--- a/arduino/builder/builder.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// This file is part of arduino-cli.
-//
-// Copyright 2020 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 builder
-
-import (
-	"crypto/md5"
-	"encoding/hex"
-	"os"
-	"strings"
-
-	"github.com/arduino/go-paths-helper"
-	"github.com/pkg/errors"
-)
-
-// GenBuildPath generates a suitable name for the build folder.
-// The sketchPath, if not nil, is also used to furhter differentiate build paths.
-func GenBuildPath(sketchPath *paths.Path) *paths.Path {
-	path := ""
-	if sketchPath != nil {
-		path = sketchPath.String()
-	}
-	md5SumBytes := md5.Sum([]byte(path))
-	md5Sum := strings.ToUpper(hex.EncodeToString(md5SumBytes[:]))
-	return paths.TempDir().Join("arduino-sketch-" + md5Sum)
-}
-
-// EnsureBuildPathExists creates the build path if doesn't already exists.
-func EnsureBuildPathExists(path string) error {
-	if err := os.MkdirAll(path, os.FileMode(0755)); err != nil {
-		return errors.Wrap(err, "unable to create build path")
-	}
-	return nil
-}
diff --git a/arduino/builder/builder_test.go b/arduino/builder/builder_test.go
deleted file mode 100644
index 44cf1e73771..00000000000
--- a/arduino/builder/builder_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// This file is part of arduino-cli.
-//
-// Copyright 2020 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 builder_test
-
-import (
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/arduino/arduino-cli/arduino/builder"
-	"github.com/arduino/go-paths-helper"
-	"github.com/stretchr/testify/assert"
-)
-
-func tmpDirOrDie() string {
-	dir, err := ioutil.TempDir(os.TempDir(), "builder_test")
-	if err != nil {
-		panic(fmt.Sprintf("error creating tmp dir: %v", err))
-	}
-	return dir
-}
-
-func TestGenBuildPath(t *testing.T) {
-	want := paths.TempDir().Join("arduino-sketch-ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
-	assert.True(t, builder.GenBuildPath(paths.New("foo")).EquivalentTo(want))
-
-	want = paths.TempDir().Join("arduino-sketch-D41D8CD98F00B204E9800998ECF8427E")
-	assert.True(t, builder.GenBuildPath(nil).EquivalentTo(want))
-}
-
-func TestEnsureBuildPathExists(t *testing.T) {
-	tmp := tmpDirOrDie()
-	defer os.RemoveAll(tmp)
-	bp := filepath.Join(tmp, "build_path")
-
-	assert.Nil(t, builder.EnsureBuildPathExists(bp))
-	_, err := os.Stat(bp)
-	assert.Nil(t, err)
-
-	// run again over an existing folder
-	assert.Nil(t, builder.EnsureBuildPathExists(bp))
-	_, err = os.Stat(bp)
-	assert.Nil(t, err)
-}
diff --git a/arduino/builder/sketch.go b/arduino/builder/sketch.go
index a1a761f795d..7b192fab37a 100644
--- a/arduino/builder/sketch.go
+++ b/arduino/builder/sketch.go
@@ -17,24 +17,16 @@ package builder
 
 import (
 	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
+	"fmt"
 	"regexp"
 	"strings"
 
-	"github.com/arduino/arduino-cli/arduino/globals"
 	"github.com/arduino/arduino-cli/arduino/sketch"
-	"github.com/arduino/arduino-cli/cli/errorcodes"
-	"github.com/arduino/arduino-cli/cli/feedback"
+	"github.com/arduino/go-paths-helper"
 
 	"github.com/pkg/errors"
 )
 
-// As currently implemented on Linux,
-// the maximum number of symbolic links that will be followed while resolving a pathname is 40
-const maxFileSystemDepth = 40
-
 var includesArduinoH = regexp.MustCompile(`(?m)^\s*#\s*include\s*[<\"]Arduino\.h[>\"]`)
 
 // QuoteCppString returns the given string as a quoted string for use with the C
@@ -47,188 +39,39 @@ func QuoteCppString(str string) string {
 }
 
 // SketchSaveItemCpp saves a preprocessed .cpp sketch file on disk
-func SketchSaveItemCpp(path string, contents []byte, destPath string) error {
-
-	sketchName := filepath.Base(path)
-
-	if err := os.MkdirAll(destPath, os.FileMode(0755)); err != nil {
+func SketchSaveItemCpp(path *paths.Path, contents []byte, destPath *paths.Path) error {
+	sketchName := path.Base()
+	if err := destPath.MkdirAll(); err != nil {
 		return errors.Wrap(err, "unable to create a folder to save the sketch")
 	}
 
-	destFile := filepath.Join(destPath, sketchName+".cpp")
+	destFile := destPath.Join(fmt.Sprintf("%s.cpp", sketchName))
 
-	if err := ioutil.WriteFile(destFile, contents, os.FileMode(0644)); err != nil {
+	if err := destFile.WriteFile(contents); err != nil {
 		return errors.Wrap(err, "unable to save the sketch on disk")
 	}
 
 	return nil
 }
 
-// simpleLocalWalk locally replaces filepath.Walk and/but goes through symlinks
-func simpleLocalWalk(root string, maxDepth int, walkFn func(path string, info os.FileInfo, err error) error) error {
-
-	info, err := os.Stat(root)
-
-	if err != nil {
-		return walkFn(root, nil, err)
-	}
-
-	err = walkFn(root, info, err)
-	if err == filepath.SkipDir {
-		return nil
-	}
-
-	if info.IsDir() {
-		if maxDepth <= 0 {
-			return walkFn(root, info, errors.New("Filesystem bottom is too deep (directory recursion or filesystem really deep): "+root))
-		}
-		maxDepth--
-		files, err := ioutil.ReadDir(root)
-		if err == nil {
-			for _, file := range files {
-				err = simpleLocalWalk(root+string(os.PathSeparator)+file.Name(), maxDepth, walkFn)
-				if err == filepath.SkipDir {
-					return nil
-				}
-			}
-		}
-	}
-
-	return nil
-}
-
-// SketchLoad collects all the files composing a sketch.
-// The parameter `sketchPath` holds a path pointing to a single sketch file or a sketch folder,
-// the path must be absolute.
-func SketchLoad(sketchPath, buildPath string) (*sketch.Sketch, error) {
-	stat, err := os.Stat(sketchPath)
-	if err != nil {
-		return nil, errors.Wrap(err, "unable to stat Sketch location")
-	}
-
-	var sketchFolder, mainSketchFile string
-
-	// if a sketch folder was passed, save the parent and point sketchPath to the main sketch file
-	if stat.IsDir() {
-		sketchFolder = sketchPath
-		// allowed extensions are .ino and .pde (but not both)
-		for extension := range globals.MainFileValidExtensions {
-			candidateSketchFile := filepath.Join(sketchPath, stat.Name()+extension)
-			if _, err := os.Stat(candidateSketchFile); !os.IsNotExist(err) {
-				if mainSketchFile == "" {
-					mainSketchFile = candidateSketchFile
-				} else {
-					return nil, errors.Errorf("multiple main sketch files found (%v,%v)",
-						filepath.Base(mainSketchFile),
-						filepath.Base(candidateSketchFile))
-				}
-			}
-		}
-
-		// check main file was found
-		if mainSketchFile == "" {
-			return nil, errors.Errorf("unable to find a sketch file in directory %v", sketchFolder)
-		}
-
-		// check main file is readable
-		f, err := os.Open(mainSketchFile)
-		if err != nil {
-			return nil, errors.Wrap(err, "unable to open the main sketch file")
-		}
-		f.Close()
-
-		// ensure it is not a directory
-		info, err := os.Stat(mainSketchFile)
-		if err != nil {
-			return nil, errors.Wrap(err, "unable to check the main sketch file")
-		}
-		if info.IsDir() {
-			return nil, errors.Wrap(errors.New(mainSketchFile), "sketch must not be a directory")
-		}
-	} else {
-		sketchFolder = filepath.Dir(sketchPath)
-		mainSketchFile = sketchPath
-	}
-
-	// collect all the sketch files
-	var files []string
-	rootVisited := false
-	err = simpleLocalWalk(sketchFolder, maxFileSystemDepth, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			feedback.Errorf("Error during sketch processing: %v", err)
-			os.Exit(errorcodes.ErrGeneric)
-		}
-
-		if info.IsDir() {
-			// Filters in this if-block are NOT applied to the sketch folder itself.
-			// Since the sketch folder is the first one processed by simpleLocalWalk,
-			// we can set the `rootVisited` guard to exclude it.
-			if rootVisited {
-				// skip hidden folders
-				if strings.HasPrefix(info.Name(), ".") {
-					return filepath.SkipDir
-				}
-
-				// skip legacy SCM directories
-				if info.Name() == "CVS" || info.Name() == "RCS" {
-					return filepath.SkipDir
-				}
-			} else {
-				rootVisited = true
-			}
-
-			// ignore (don't skip) directory
-			return nil
-		}
-
-		// ignore hidden files
-		if strings.HasPrefix(info.Name(), ".") {
-			return nil
-		}
-
-		// ignore if file extension doesn't match
-		ext := filepath.Ext(path)
-		_, isMain := globals.MainFileValidExtensions[ext]
-		_, isAdditional := globals.AdditionalFileValidExtensions[ext]
-		if !(isMain || isAdditional) {
-			return nil
-		}
-
-		// check if file is readable
-		f, err := os.Open(path)
-		if err != nil {
-			return nil
-		}
-		f.Close()
-
-		// collect the file
-		files = append(files, path)
-
-		// done
-		return nil
-	})
-
-	if err != nil {
-		return nil, errors.Wrap(err, "there was an error while collecting the sketch files")
-	}
-
-	return sketch.New(sketchFolder, mainSketchFile, buildPath, files)
-}
-
 // SketchMergeSources merges all the source files included in a sketch
 func SketchMergeSources(sk *sketch.Sketch, overrides map[string]string) (int, string, error) {
 	lineOffset := 0
 	mergedSource := ""
 
-	getSource := func(i *sketch.Item) (string, error) {
-		path, err := filepath.Rel(sk.LocationPath, i.Path)
+	getSource := func(f *paths.Path) (string, error) {
+		path, err := sk.FullPath.RelTo(f)
 		if err != nil {
 			return "", errors.Wrap(err, "unable to compute relative path to the sketch for the item")
 		}
-		if override, ok := overrides[path]; ok {
+		if override, ok := overrides[path.String()]; ok {
 			return override, nil
 		}
-		return i.GetSourceStr()
+		data, err := f.ReadFile()
+		if err != nil {
+			return "", fmt.Errorf("reading file %s: %s", f, err)
+		}
+		return string(data), nil
 	}
 
 	// add Arduino.h inclusion directive if missing
@@ -241,16 +84,16 @@ func SketchMergeSources(sk *sketch.Sketch, overrides map[string]string) (int, st
 		lineOffset++
 	}
 
-	mergedSource += "#line 1 " + QuoteCppString(sk.MainFile.Path) + "\n"
+	mergedSource += "#line 1 " + QuoteCppString(sk.MainFile.String()) + "\n"
 	mergedSource += mainSrc + "\n"
 	lineOffset++
 
-	for _, item := range sk.OtherSketchFiles {
-		src, err := getSource(item)
+	for _, file := range *sk.OtherSketchFiles {
+		src, err := getSource(file)
 		if err != nil {
 			return 0, "", err
 		}
-		mergedSource += "#line 1 " + QuoteCppString(item.Path) + "\n"
+		mergedSource += "#line 1 " + QuoteCppString(file.String()) + "\n"
 		mergedSource += src + "\n"
 	}
 
@@ -259,30 +102,30 @@ func SketchMergeSources(sk *sketch.Sketch, overrides map[string]string) (int, st
 
 // SketchCopyAdditionalFiles copies the additional files for a sketch to the
 // specified destination directory.
-func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath string, overrides map[string]string) error {
-	if err := os.MkdirAll(destPath, os.FileMode(0755)); err != nil {
+func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath *paths.Path, overrides map[string]string) error {
+	if err := destPath.MkdirAll(); err != nil {
 		return errors.Wrap(err, "unable to create a folder to save the sketch files")
 	}
 
-	for _, item := range sketch.AdditionalFiles {
-		relpath, err := filepath.Rel(sketch.LocationPath, item.Path)
+	for _, file := range *sketch.AdditionalFiles {
+		relpath, err := sketch.FullPath.RelTo(file)
 		if err != nil {
 			return errors.Wrap(err, "unable to compute relative path to the sketch for the item")
 		}
 
-		targetPath := filepath.Join(destPath, relpath)
+		targetPath := destPath.JoinPath(relpath)
 		// create the directory containing the target
-		if err = os.MkdirAll(filepath.Dir(targetPath), os.FileMode(0755)); err != nil {
+		if err = targetPath.Parent().MkdirAll(); err != nil {
 			return errors.Wrap(err, "unable to create the folder containing the item")
 		}
 
 		var sourceBytes []byte
-		if override, ok := overrides[relpath]; ok {
+		if override, ok := overrides[relpath.String()]; ok {
 			// use override source
 			sourceBytes = []byte(override)
 		} else {
 			// read the source file
-			s, err := item.GetSourceBytes()
+			s, err := file.ReadFile()
 			if err != nil {
 				return errors.Wrap(err, "unable to read contents of the source item")
 			}
@@ -290,7 +133,7 @@ func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath string, overrides
 		}
 
 		// tag each addtional file with the filename of the source it was copied from
-		sourceBytes = append([]byte("#line 1 "+QuoteCppString(item.Path)+"\n"), sourceBytes...)
+		sourceBytes = append([]byte("#line 1 "+QuoteCppString(file.String())+"\n"), sourceBytes...)
 
 		err = writeIfDifferent(sourceBytes, targetPath)
 		if err != nil {
@@ -301,25 +144,24 @@ func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath string, overrides
 	return nil
 }
 
-func writeIfDifferent(source []byte, destPath string) error {
-	// check whether the destination file exists
-	_, err := os.Stat(destPath)
-	if os.IsNotExist(err) {
-		// write directly
-		return ioutil.WriteFile(destPath, source, os.FileMode(0644))
+func writeIfDifferent(source []byte, destPath *paths.Path) error {
+	// Check whether the destination file exists
+	if destPath.NotExist() {
+		// Write directly
+		return destPath.WriteFile(source)
 	}
 
-	// read the destination file if it ex
-	existingBytes, err := ioutil.ReadFile(destPath)
+	// Read the destination file if it exists
+	existingBytes, err := destPath.ReadFile()
 	if err != nil {
 		return errors.Wrap(err, "unable to read contents of the destination item")
 	}
 
-	// overwrite if contents are different
+	// Overwrite if contents are different
 	if bytes.Compare(existingBytes, source) != 0 {
-		return ioutil.WriteFile(destPath, source, os.FileMode(0644))
+		return destPath.WriteFile(source)
 	}
 
-	// source and destination are the same, don't write anything
+	// Source and destination are the same, don't write anything
 	return nil
 }
diff --git a/arduino/builder/sketch_test.go b/arduino/builder/sketch_test.go
index 7b535203d6d..d7fd40b22c5 100644
--- a/arduino/builder/sketch_test.go
+++ b/arduino/builder/sketch_test.go
@@ -25,23 +25,33 @@ import (
 	"testing"
 
 	"github.com/arduino/arduino-cli/arduino/builder"
+	"github.com/arduino/arduino-cli/arduino/sketch"
+	"github.com/arduino/go-paths-helper"
 	"github.com/stretchr/testify/require"
 )
 
+func tmpDirOrDie() *paths.Path {
+	dir, err := ioutil.TempDir(os.TempDir(), "builder_test")
+	if err != nil {
+		panic(fmt.Sprintf("error creating tmp dir: %v", err))
+	}
+	return paths.New(dir)
+}
+
 func TestSaveSketch(t *testing.T) {
 	sketchName := t.Name() + ".ino"
 	outName := sketchName + ".cpp"
 	sketchFile := filepath.Join("testdata", sketchName)
 	tmp := tmpDirOrDie()
-	defer os.RemoveAll(tmp)
+	defer tmp.RemoveAll()
 	source, err := ioutil.ReadFile(sketchFile)
 	if err != nil {
 		t.Fatalf("unable to read golden file %s: %v", sketchFile, err)
 	}
 
-	builder.SketchSaveItemCpp(sketchName, source, tmp)
+	builder.SketchSaveItemCpp(paths.New(sketchName), source, tmp)
 
-	out, err := ioutil.ReadFile(filepath.Join(tmp, outName))
+	out, err := tmp.Join(outName).ReadFile()
 	if err != nil {
 		t.Fatalf("unable to read output file %s: %v", outName, err)
 	}
@@ -49,148 +59,9 @@ func TestSaveSketch(t *testing.T) {
 	require.Equal(t, source, out)
 }
 
-func TestLoadSketchFolder(t *testing.T) {
-	// pass the path to the sketch folder
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
-	s, err := builder.SketchLoad(sketchPath, "")
-	require.Nil(t, err)
-	require.NotNil(t, s)
-	require.Equal(t, mainFilePath, s.MainFile.Path)
-	require.Equal(t, sketchPath, s.LocationPath)
-	require.Len(t, s.OtherSketchFiles, 2)
-	require.Equal(t, "old.pde", filepath.Base(s.OtherSketchFiles[0].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.OtherSketchFiles[1].Path))
-	require.Len(t, s.AdditionalFiles, 3)
-	require.Equal(t, "header.h", filepath.Base(s.AdditionalFiles[0].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.AdditionalFiles[1].Path))
-	require.Equal(t, "helper.h", filepath.Base(s.AdditionalFiles[2].Path))
-	require.Len(t, s.RootFolderFiles, 4)
-	require.Equal(t, "header.h", filepath.Base(s.RootFolderFiles[0].Path))
-	require.Equal(t, "old.pde", filepath.Base(s.RootFolderFiles[1].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.RootFolderFiles[2].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.RootFolderFiles[3].Path))
-
-	// pass the path to the main file
-	sketchPath = mainFilePath
-	s, err = builder.SketchLoad(sketchPath, "")
-	require.Nil(t, err)
-	require.NotNil(t, s)
-	require.Equal(t, mainFilePath, s.MainFile.Path)
-	require.Len(t, s.OtherSketchFiles, 2)
-	require.Equal(t, "old.pde", filepath.Base(s.OtherSketchFiles[0].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.OtherSketchFiles[1].Path))
-	require.Len(t, s.AdditionalFiles, 3)
-	require.Equal(t, "header.h", filepath.Base(s.AdditionalFiles[0].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.AdditionalFiles[1].Path))
-	require.Equal(t, "helper.h", filepath.Base(s.AdditionalFiles[2].Path))
-	require.Len(t, s.RootFolderFiles, 4)
-	require.Equal(t, "header.h", filepath.Base(s.RootFolderFiles[0].Path))
-	require.Equal(t, "old.pde", filepath.Base(s.RootFolderFiles[1].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.RootFolderFiles[2].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.RootFolderFiles[3].Path))
-}
-
-func TestLoadSketchFolderPde(t *testing.T) {
-	// pass the path to the sketch folder
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".pde")
-	s, err := builder.SketchLoad(sketchPath, "")
-	require.Nil(t, err)
-	require.NotNil(t, s)
-	require.Equal(t, mainFilePath, s.MainFile.Path)
-	require.Equal(t, sketchPath, s.LocationPath)
-	require.Len(t, s.OtherSketchFiles, 2)
-	require.Equal(t, "old.pde", filepath.Base(s.OtherSketchFiles[0].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.OtherSketchFiles[1].Path))
-	require.Len(t, s.AdditionalFiles, 3)
-	require.Equal(t, "header.h", filepath.Base(s.AdditionalFiles[0].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.AdditionalFiles[1].Path))
-	require.Equal(t, "helper.h", filepath.Base(s.AdditionalFiles[2].Path))
-	require.Len(t, s.RootFolderFiles, 4)
-	require.Equal(t, "header.h", filepath.Base(s.RootFolderFiles[0].Path))
-	require.Equal(t, "old.pde", filepath.Base(s.RootFolderFiles[1].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.RootFolderFiles[2].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.RootFolderFiles[3].Path))
-}
-
-func TestLoadSketchFolderBothInoAndPde(t *testing.T) {
-	// pass the path to the sketch folder containing two main sketches, .ino and .pde
-	sketchPath := filepath.Join("testdata", t.Name())
-	_, err := builder.SketchLoad(sketchPath, "")
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "multiple main sketch files found")
-	require.Contains(t, err.Error(), t.Name()+".ino")
-	require.Contains(t, err.Error(), t.Name()+".pde")
-}
-
-func TestLoadSketchFolderSymlink(t *testing.T) {
-	// pass the path to the sketch folder
-	symlinkSketchPath := filepath.Join("testdata", t.Name())
-	srcSketchPath := t.Name() + "Src"
-	os.Symlink(srcSketchPath, symlinkSketchPath)
-	defer os.Remove(symlinkSketchPath)
-	mainFilePath := filepath.Join(symlinkSketchPath, t.Name()+".ino")
-	s, err := builder.SketchLoad(symlinkSketchPath, "")
-	require.Nil(t, err)
-	require.NotNil(t, s)
-	require.Equal(t, mainFilePath, s.MainFile.Path)
-	require.Equal(t, symlinkSketchPath, s.LocationPath)
-	require.Len(t, s.OtherSketchFiles, 2)
-	require.Equal(t, "old.pde", filepath.Base(s.OtherSketchFiles[0].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.OtherSketchFiles[1].Path))
-	require.Len(t, s.AdditionalFiles, 3)
-	require.Equal(t, "header.h", filepath.Base(s.AdditionalFiles[0].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.AdditionalFiles[1].Path))
-	require.Equal(t, "helper.h", filepath.Base(s.AdditionalFiles[2].Path))
-	require.Len(t, s.RootFolderFiles, 4)
-	require.Equal(t, "header.h", filepath.Base(s.RootFolderFiles[0].Path))
-	require.Equal(t, "old.pde", filepath.Base(s.RootFolderFiles[1].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.RootFolderFiles[2].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.RootFolderFiles[3].Path))
-
-	// pass the path to the main file
-	symlinkSketchPath = mainFilePath
-	s, err = builder.SketchLoad(symlinkSketchPath, "")
-	require.Nil(t, err)
-	require.NotNil(t, s)
-	require.Equal(t, mainFilePath, s.MainFile.Path)
-	require.Len(t, s.OtherSketchFiles, 2)
-	require.Equal(t, "old.pde", filepath.Base(s.OtherSketchFiles[0].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.OtherSketchFiles[1].Path))
-	require.Len(t, s.AdditionalFiles, 3)
-	require.Equal(t, "header.h", filepath.Base(s.AdditionalFiles[0].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.AdditionalFiles[1].Path))
-	require.Equal(t, "helper.h", filepath.Base(s.AdditionalFiles[2].Path))
-	require.Len(t, s.RootFolderFiles, 4)
-	require.Equal(t, "header.h", filepath.Base(s.RootFolderFiles[0].Path))
-	require.Equal(t, "old.pde", filepath.Base(s.RootFolderFiles[1].Path))
-	require.Equal(t, "other.ino", filepath.Base(s.RootFolderFiles[2].Path))
-	require.Equal(t, "s_file.S", filepath.Base(s.RootFolderFiles[3].Path))
-}
-
-func TestLoadSketchFolderIno(t *testing.T) {
-	// pass the path to the sketch folder
-	sketchPath := filepath.Join("testdata", t.Name())
-	_, err := builder.SketchLoad(sketchPath, "")
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "sketch must not be a directory")
-}
-
-func TestLoadSketchFolderWrongMain(t *testing.T) {
-	sketchPath := filepath.Join("testdata", t.Name())
-	_, err := builder.SketchLoad(sketchPath, "")
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "unable to find a sketch file in directory testdata")
-
-	_, err = builder.SketchLoad("does/not/exist", "")
-	require.Error(t, err)
-	require.Contains(t, err.Error(), "does/not/exist")
-}
-
 func TestMergeSketchSources(t *testing.T) {
 	// borrow the sketch from TestLoadSketchFolder to avoid boilerplate
-	s, err := builder.SketchLoad(filepath.Join("testdata", "TestLoadSketchFolder"), "")
+	s, err := sketch.New(paths.New("testdata", "TestLoadSketchFolder"))
 	require.Nil(t, err)
 	require.NotNil(t, s)
 
@@ -199,20 +70,27 @@ func TestMergeSketchSources(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		suffix = "_win.txt"
 	}
-	mergedPath := filepath.Join("testdata", t.Name()+suffix)
-	mergedBytes, err := ioutil.ReadFile(mergedPath)
+	mergedPath := paths.New("testdata", t.Name()+suffix)
+	mergedBytes, err := mergedPath.ReadFile()
 	if err != nil {
 		t.Fatalf("unable to read golden file %s: %v", mergedPath, err)
 	}
 
+	mergedPath.ToAbs()
+	pathToGoldenSource := mergedPath.Parent().Parent().String()
+	if runtime.GOOS == "windows" {
+		pathToGoldenSource = strings.ReplaceAll(pathToGoldenSource, `\`, `\\`)
+	}
+	mergedSources := strings.ReplaceAll(string(mergedBytes), "%s", pathToGoldenSource)
+
 	offset, source, err := builder.SketchMergeSources(s, nil)
 	require.Nil(t, err)
 	require.Equal(t, 2, offset)
-	require.Equal(t, string(mergedBytes), source)
+	require.Equal(t, mergedSources, source)
 }
 
 func TestMergeSketchSourcesArduinoIncluded(t *testing.T) {
-	s, err := builder.SketchLoad(filepath.Join("testdata", t.Name()), "")
+	s, err := sketch.New(paths.New("testdata", t.Name()))
 	require.Nil(t, err)
 	require.NotNil(t, s)
 
@@ -224,27 +102,27 @@ func TestMergeSketchSourcesArduinoIncluded(t *testing.T) {
 
 func TestCopyAdditionalFiles(t *testing.T) {
 	tmp := tmpDirOrDie()
-	defer os.RemoveAll(tmp)
+	defer tmp.RemoveAll()
 
 	// load the golden sketch
-	s1, err := builder.SketchLoad(filepath.Join("testdata", t.Name()), "")
+	s1, err := sketch.New(paths.New("testdata", t.Name()))
 	require.Nil(t, err)
-	require.Len(t, s1.AdditionalFiles, 1)
+	require.Equal(t, s1.AdditionalFiles.Len(), 1)
 
 	// copy the sketch over, create a fake main file we don't care about it
 	// but we need it for `SketchLoad` to succeed later
 	err = builder.SketchCopyAdditionalFiles(s1, tmp, nil)
 	require.Nil(t, err)
-	fakeIno := filepath.Join(tmp, fmt.Sprintf("%s.ino", filepath.Base(tmp)))
-	require.Nil(t, ioutil.WriteFile(fakeIno, []byte{}, os.FileMode(0644)))
+	fakeIno := tmp.Join(fmt.Sprintf("%s.ino", tmp.Base()))
+	require.Nil(t, fakeIno.WriteFile([]byte{}))
 
 	// compare
-	s2, err := builder.SketchLoad(tmp, "")
+	s2, err := sketch.New(tmp)
 	require.Nil(t, err)
-	require.Len(t, s2.AdditionalFiles, 1)
+	require.Equal(t, s2.AdditionalFiles.Len(), 1)
 
 	// save file info
-	info1, err := os.Stat(s2.AdditionalFiles[0].Path)
+	info1, err := paths.New(s2.AdditionalFiles.AsStrings()[0]).Stat()
 	require.Nil(t, err)
 
 	// copy again
@@ -252,72 +130,6 @@ func TestCopyAdditionalFiles(t *testing.T) {
 	require.Nil(t, err)
 
 	// verify file hasn't changed
-	info2, err := os.Stat(s2.AdditionalFiles[0].Path)
+	info2, err := paths.New(s2.AdditionalFiles.AsStrings()[0]).Stat()
 	require.Equal(t, info1.ModTime(), info2.ModTime())
 }
-
-func TestLoadSketchCaseMismatch(t *testing.T) {
-	// pass the path to the sketch folder
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
-	s, err := builder.SketchLoad(sketchPath, "")
-	require.Nil(t, s)
-	require.Error(t, err)
-
-	// pass the path to the main file
-	s, err = builder.SketchLoad(mainFilePath, "")
-	require.Nil(t, s)
-	require.Error(t, err)
-}
-
-func TestSketchWithMarkdownAsciidocJson(t *testing.T) {
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
-
-	sketch, err := builder.SketchLoad(sketchPath, "")
-	require.NotNil(t, sketch)
-	require.NoError(t, err)
-	require.Equal(t, sketchPath, sketch.LocationPath)
-	require.Equal(t, mainFilePath, sketch.MainFile.Path)
-	require.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 3)
-	require.Equal(t, "foo.adoc", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Equal(t, "foo.json", filepath.Base(sketch.AdditionalFiles[1].Path))
-	require.Equal(t, "foo.md", filepath.Base(sketch.AdditionalFiles[2].Path))
-	require.Len(t, sketch.RootFolderFiles, 3)
-	require.Equal(t, "foo.adoc", filepath.Base(sketch.RootFolderFiles[0].Path))
-	require.Equal(t, "foo.json", filepath.Base(sketch.RootFolderFiles[1].Path))
-	require.Equal(t, "foo.md", filepath.Base(sketch.RootFolderFiles[2].Path))
-}
-
-func TestSketchWithTppFile(t *testing.T) {
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
-
-	sketch, err := builder.SketchLoad(sketchPath, "")
-	require.NotNil(t, sketch)
-	require.NoError(t, err)
-	require.Equal(t, sketchPath, sketch.LocationPath)
-	require.Equal(t, mainFilePath, sketch.MainFile.Path)
-	require.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 1)
-	require.Equal(t, "template.tpp", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Len(t, sketch.RootFolderFiles, 1)
-	require.Equal(t, "template.tpp", filepath.Base(sketch.RootFolderFiles[0].Path))
-}
-
-func TestSketchWithIppFile(t *testing.T) {
-	sketchPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
-
-	sketch, err := builder.SketchLoad(sketchPath, "")
-	require.NotNil(t, sketch)
-	require.NoError(t, err)
-	require.Equal(t, sketchPath, sketch.LocationPath)
-	require.Equal(t, mainFilePath, sketch.MainFile.Path)
-	require.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 1)
-	require.Equal(t, "template.ipp", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Len(t, sketch.RootFolderFiles, 1)
-	require.Equal(t, "template.ipp", filepath.Base(sketch.RootFolderFiles[0].Path))
-}
diff --git a/arduino/builder/testdata/TestMergeSketchSources.txt b/arduino/builder/testdata/TestMergeSketchSources.txt
index 57f68974397..7021957c534 100644
--- a/arduino/builder/testdata/TestMergeSketchSources.txt
+++ b/arduino/builder/testdata/TestMergeSketchSources.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 "testdata/TestLoadSketchFolder/TestLoadSketchFolder.ino"
+#line 1 "%s/testdata/TestLoadSketchFolder/TestLoadSketchFolder.ino"
 void setup() {
 
 }
@@ -7,9 +7,9 @@ void setup() {
 void loop() {
 
 }
-#line 1 "testdata/TestLoadSketchFolder/old.pde"
+#line 1 "%s/testdata/TestLoadSketchFolder/old.pde"
 
-#line 1 "testdata/TestLoadSketchFolder/other.ino"
+#line 1 "%s/testdata/TestLoadSketchFolder/other.ino"
 String hello() {
   return "world";
 }
diff --git a/arduino/builder/testdata/TestMergeSketchSources_win.txt b/arduino/builder/testdata/TestMergeSketchSources_win.txt
index 933987f7b60..4eed9b8eefd 100644
--- a/arduino/builder/testdata/TestMergeSketchSources_win.txt
+++ b/arduino/builder/testdata/TestMergeSketchSources_win.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 "testdata\\TestLoadSketchFolder\\TestLoadSketchFolder.ino"
+#line 1 "%s\\testdata\\TestLoadSketchFolder\\TestLoadSketchFolder.ino"
 void setup() {
 
 }
@@ -7,9 +7,9 @@ void setup() {
 void loop() {
 
 }
-#line 1 "testdata\\TestLoadSketchFolder\\old.pde"
+#line 1 "%s\\testdata\\TestLoadSketchFolder\\old.pde"
 
-#line 1 "testdata\\TestLoadSketchFolder\\other.ino"
+#line 1 "%s\\testdata\\TestLoadSketchFolder\\other.ino"
 String hello() {
   return "world";
 }
diff --git a/arduino/libraries/loader.go b/arduino/libraries/loader.go
index 0546229745a..482ff39a78c 100644
--- a/arduino/libraries/loader.go
+++ b/arduino/libraries/loader.go
@@ -19,7 +19,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/go-paths-helper"
 	properties "github.com/arduino/go-properties-orderedmap"
 	"github.com/pkg/errors"
@@ -173,7 +173,7 @@ func addExamplesToPathList(examplesPath *paths.Path, list *paths.PathList) error
 		return err
 	}
 	for _, file := range files {
-		_, err := sketches.NewSketchFromPath(file)
+		_, err := sketch.New(file)
 		if err == nil {
 			list.Add(file)
 		} else if file.IsDir() {
diff --git a/arduino/sketch/sketch.go b/arduino/sketch/sketch.go
index 9de4fdfd077..2991f2ad920 100644
--- a/arduino/sketch/sketch.go
+++ b/arduino/sketch/sketch.go
@@ -16,9 +16,10 @@
 package sketch
 
 import (
+	"crypto/md5"
+	"encoding/hex"
+	"encoding/json"
 	"fmt"
-	"io/ioutil"
-	"path/filepath"
 	"sort"
 	"strings"
 
@@ -27,123 +28,189 @@ import (
 	"github.com/pkg/errors"
 )
 
-// Item holds the source and the path for a single sketch file
-type Item struct {
-	Path string
+// Sketch holds all the files composing a sketch
+type Sketch struct {
+	Name             string
+	MainFile         *paths.Path
+	FullPath         *paths.Path // FullPath is the path to the Sketch folder
+	BuildPath        *paths.Path
+	OtherSketchFiles *paths.PathList // Sketch files that end in .ino other than main file
+	AdditionalFiles  *paths.PathList
+	RootFolderFiles  *paths.PathList // All files that are in the Sketch root
+	Metadata         *Metadata
 }
 
-// NewItem reads the source code for a sketch item and returns an
-// Item instance
-func NewItem(itemPath string) *Item {
-	return &Item{itemPath}
+// Metadata is the kind of data associated to a project such as the connected board
+type Metadata struct {
+	CPU BoardMetadata `json:"cpu,omitempty"`
 }
 
-// GetSourceBytes reads the item file contents and returns it as bytes
-func (i *Item) GetSourceBytes() ([]byte, error) {
-	// read the file
-	source, err := ioutil.ReadFile(i.Path)
-	if err != nil {
-		return nil, errors.Wrap(err, "error reading source file")
-	}
-	return source, nil
+// BoardMetadata represents the board metadata for the sketch
+type BoardMetadata struct {
+	Fqbn string `json:"fqbn,required"`
+	Name string `json:"name,omitempty"`
+	Port string `json:"port,omitepty"`
 }
 
-// GetSourceStr reads the item file contents and returns it as a string
-func (i *Item) GetSourceStr() (string, error) {
-	source, err := i.GetSourceBytes()
-	if err != nil {
-		return "", err
+// New creates an Sketch instance by reading all the files composing a sketch and grouping them
+// by file type.
+func New(path *paths.Path) (*Sketch, error) {
+	path = path.Canonical()
+	if !path.IsDir() {
+		path = path.Parent()
 	}
-	return string(source), nil
-}
 
-// ItemByPath implements sort.Interface for []Item based on
-// lexicographic order of the path string.
-type ItemByPath []*Item
+	var mainFile *paths.Path
+	for ext := range globals.MainFileValidExtensions {
+		candidateSketchMainFile := path.Join(path.Base() + ext)
+		if candidateSketchMainFile.Exist() {
+			if mainFile == nil {
+				mainFile = candidateSketchMainFile
+			} else {
+				return nil, errors.Errorf("multiple main sketch files found (%v, %v)",
+					mainFile,
+					candidateSketchMainFile,
+				)
+			}
+		}
+	}
 
-func (ibn ItemByPath) Len() int           { return len(ibn) }
-func (ibn ItemByPath) Swap(i, j int)      { ibn[i], ibn[j] = ibn[j], ibn[i] }
-func (ibn ItemByPath) Less(i, j int) bool { return ibn[i].Path < ibn[j].Path }
+	sketch := &Sketch{
+		Name:             path.Base(),
+		MainFile:         mainFile,
+		FullPath:         path,
+		BuildPath:        GenBuildPath(path),
+		OtherSketchFiles: new(paths.PathList),
+		AdditionalFiles:  new(paths.PathList),
+		RootFolderFiles:  new(paths.PathList),
+	}
 
-// Sketch holds all the files composing a sketch
-type Sketch struct {
-	MainFile         *Item
-	LocationPath     string
-	OtherSketchFiles []*Item
-	AdditionalFiles  []*Item
-	RootFolderFiles  []*Item
-}
+	err := sketch.checkSketchCasing()
+	if e, ok := err.(*InvalidSketchFolderNameError); ok {
+		return nil, e
+	}
+	if err != nil {
+		return nil, err
+	}
 
-// New creates an Sketch instance by reading all the files composing a sketch and grouping them
-// by file type.
-func New(sketchFolderPath, mainFilePath, buildPath string, allFilesPaths []string) (*Sketch, error) {
-	var mainFile *Item
-
-	// read all the sketch contents and create sketch Items
-	pathToItem := make(map[string]*Item)
-	for _, p := range allFilesPaths {
-		// create an Item
-		item := NewItem(p)
-
-		if p == mainFilePath {
-			// store the main sketch file
-			mainFile = item
-		} else {
-			// map the file path to sketch.Item
-			pathToItem[p] = item
-		}
+	if mainFile == nil {
+		return nil, fmt.Errorf("can't find main Sketch file in %s", path)
 	}
 
-	// organize the Items
-	additionalFiles := []*Item{}
-	otherSketchFiles := []*Item{}
-	rootFolderFiles := []*Item{}
-	for p, item := range pathToItem {
-		ext := filepath.Ext(p)
+	sketchFolderFiles, err := sketch.supportedFiles()
+	if err != nil {
+		return nil, err
+	}
+
+	// Collect files
+	for _, p := range *sketchFolderFiles {
+		// Skip files that can't be opened
+		f, err := p.Open()
+		if err != nil {
+			continue
+		}
+		f.Close()
+
+		ext := p.Ext()
 		if _, found := globals.MainFileValidExtensions[ext]; found {
-			// item is a valid main file, see if it's stored at the
+			if p.EqualsTo(mainFile) {
+				// The main file must be included in the lists of other files
+				continue
+			}
+			// file is a valid sketch file, see if it's stored at the
 			// sketch root and ignore if it's not.
-			if filepath.Dir(p) == sketchFolderPath {
-				otherSketchFiles = append(otherSketchFiles, item)
-				rootFolderFiles = append(rootFolderFiles, item)
+			if p.Parent().EqualsTo(path) {
+				sketch.OtherSketchFiles.Add(p)
+				sketch.RootFolderFiles.Add(p)
 			}
 		} else if _, found := globals.AdditionalFileValidExtensions[ext]; found {
-			// item is a valid sketch file, grab it only if the buildPath is empty
-			// or the file is within the buildPath
-			if buildPath == "" || !strings.Contains(filepath.Dir(p), buildPath) {
-				additionalFiles = append(additionalFiles, item)
-				if filepath.Dir(p) == sketchFolderPath {
-					rootFolderFiles = append(rootFolderFiles, item)
-				}
+			// If the user exported the compiles binaries to the Sketch "build" folder
+			// they would be picked up but we don't want them, so we skip them like so
+			if isInBuildFolder, err := p.IsInsideDir(sketch.FullPath.Join("build")); isInBuildFolder || err != nil {
+				continue
+			}
+
+			sketch.AdditionalFiles.Add(p)
+			if p.Parent().EqualsTo(path) {
+				sketch.RootFolderFiles.Add(p)
 			}
 		} else {
 			return nil, errors.Errorf("unknown sketch file extension '%s'", ext)
 		}
 	}
 
-	sort.Sort(ItemByPath(additionalFiles))
-	sort.Sort(ItemByPath(otherSketchFiles))
-	sort.Sort(ItemByPath(rootFolderFiles))
+	sort.Sort(sketch.AdditionalFiles)
+	sort.Sort(sketch.OtherSketchFiles)
+	sort.Sort(sketch.RootFolderFiles)
 
-	sk := &Sketch{
-		MainFile:         mainFile,
-		LocationPath:     sketchFolderPath,
-		OtherSketchFiles: otherSketchFiles,
-		AdditionalFiles:  additionalFiles,
-		RootFolderFiles:  rootFolderFiles,
-	}
-	err := CheckSketchCasing(sketchFolderPath)
-	if e, ok := err.(*InvalidSketchFoldernameError); ok {
-		e.Sketch = sk
-		return nil, e
+	if err := sketch.importMetadata(); err != nil {
+		return nil, fmt.Errorf("importing sketch metadata: %s", err)
 	}
+	return sketch, nil
+}
+
+// supportedFiles reads all files recursively contained in Sketch and
+// filter out unneded or unsupported ones and returns them
+func (s *Sketch) supportedFiles() (*paths.PathList, error) {
+	files, err := s.FullPath.ReadDirRecursive()
 	if err != nil {
 		return nil, err
 	}
-	return sk, nil
+	files.FilterOutDirs()
+	files.FilterOutHiddenFiles()
+	validExtensions := []string{}
+	for ext := range globals.MainFileValidExtensions {
+		validExtensions = append(validExtensions, ext)
+	}
+	for ext := range globals.AdditionalFileValidExtensions {
+		validExtensions = append(validExtensions, ext)
+	}
+	files.FilterSuffix(validExtensions...)
+	return &files, nil
+
+}
+
+// ImportMetadata imports metadata into the sketch from a sketch.json file in the root
+// path of the sketch.
+func (s *Sketch) importMetadata() error {
+	sketchJSON := s.FullPath.Join("sketch.json")
+	if sketchJSON.NotExist() {
+		// File doesn't exist, nothing to import
+		return nil
+	}
+
+	content, err := sketchJSON.ReadFile()
+	if err != nil {
+		return fmt.Errorf("reading sketch metadata %s: %s", sketchJSON, err)
+	}
+	var meta Metadata
+	err = json.Unmarshal(content, &meta)
+	if err != nil {
+		if s.Metadata == nil {
+			s.Metadata = new(Metadata)
+		}
+		return fmt.Errorf("encoding sketch metadata: %s", err)
+	}
+	s.Metadata = &meta
+	return nil
+}
+
+// ExportMetadata writes sketch metadata into a sketch.json file in the root path of
+// the sketch
+func (s *Sketch) ExportMetadata() error {
+	d, err := json.MarshalIndent(&s.Metadata, "", "  ")
+	if err != nil {
+		return fmt.Errorf("decoding sketch metadata: %s", err)
+	}
+
+	sketchJSON := s.FullPath.Join("sketch.json")
+	if err := sketchJSON.WriteFile(d); err != nil {
+		return fmt.Errorf("writing sketch metadata %s: %s", sketchJSON, err)
+	}
+	return nil
 }
 
-// CheckSketchCasing returns an error if the casing of the sketch folder and the main file are different.
+// checkSketchCasing returns an error if the casing of the sketch folder and the main file are different.
 // Correct:
 //    MySketch/MySketch.ino
 // Wrong:
@@ -152,33 +219,63 @@ func New(sketchFolderPath, mainFilePath, buildPath string, allFilesPaths []strin
 //
 // This is mostly necessary to avoid errors on Mac OS X.
 // For more info see: https://github.com/arduino/arduino-cli/issues/1174
-func CheckSketchCasing(sketchFolder string) error {
-	sketchPath := paths.New(sketchFolder)
-	files, err := sketchPath.ReadDir()
+func (s *Sketch) checkSketchCasing() error {
+	files, err := s.FullPath.ReadDir()
 	if err != nil {
 		return errors.Errorf("reading files: %v", err)
 	}
 	files.FilterOutDirs()
 
-	sketchName := sketchPath.Base()
-	files.FilterPrefix(sketchName)
+	files.FilterPrefix(s.Name)
 
 	if files.Len() == 0 {
-		sketchFolderPath := paths.New(sketchFolder)
-		sketchFile := sketchFolderPath.Join(sketchFolderPath.Base() + globals.MainFileValidExtension)
-		return &InvalidSketchFoldernameError{SketchFolder: sketchFolderPath, SketchFile: sketchFile}
+		sketchFile := s.FullPath.Join(s.Name + globals.MainFileValidExtension)
+		return &InvalidSketchFolderNameError{
+			SketchFolder: s.FullPath,
+			SketchFile:   sketchFile,
+			Sketch:       s,
+		}
 	}
 
 	return nil
 }
 
-// InvalidSketchFoldernameError is returned when the sketch directory doesn't match the sketch name
-type InvalidSketchFoldernameError struct {
+// InvalidSketchFolderNameError is returned when the sketch directory doesn't match the sketch name
+type InvalidSketchFolderNameError struct {
 	SketchFolder *paths.Path
 	SketchFile   *paths.Path
 	Sketch       *Sketch
 }
 
-func (e *InvalidSketchFoldernameError) Error() string {
+func (e *InvalidSketchFolderNameError) Error() string {
 	return fmt.Sprintf("no valid sketch found in %s: missing %s", e.SketchFolder, e.SketchFile)
 }
+
+// CheckForPdeFiles returns all files ending with .pde extension
+// in sketch, this is mainly used to warn the user that these files
+// must be changed to .ino extension.
+// When .pde files won't be supported anymore this function must be removed.
+func CheckForPdeFiles(sketch *paths.Path) []*paths.Path {
+	if sketch.IsNotDir() {
+		sketch = sketch.Parent()
+	}
+
+	files, err := sketch.ReadDirRecursive()
+	if err != nil {
+		return []*paths.Path{}
+	}
+	files.FilterSuffix(".pde")
+	return files
+}
+
+// GenBuildPath generates a suitable name for the build folder.
+// The sketchPath, if not nil, is also used to furhter differentiate build paths.
+func GenBuildPath(sketchPath *paths.Path) *paths.Path {
+	path := ""
+	if sketchPath != nil {
+		path = sketchPath.String()
+	}
+	md5SumBytes := md5.Sum([]byte(path))
+	md5Sum := strings.ToUpper(hex.EncodeToString(md5SumBytes[:]))
+	return paths.TempDir().Join("arduino-sketch-" + md5Sum)
+}
diff --git a/arduino/sketch/sketch_test.go b/arduino/sketch/sketch_test.go
index 8136b679645..dc53727010e 100644
--- a/arduino/sketch/sketch_test.go
+++ b/arduino/sketch/sketch_test.go
@@ -17,8 +17,7 @@ package sketch
 
 import (
 	"fmt"
-	"path/filepath"
-	"sort"
+	"os"
 	"testing"
 
 	"github.com/arduino/go-paths-helper"
@@ -26,149 +25,278 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestNewItem(t *testing.T) {
-	sketchItem := filepath.Join("testdata", t.Name()+".ino")
-	item := NewItem(sketchItem)
-	assert.Equal(t, sketchItem, item.Path)
-	sourceBytes, err := item.GetSourceBytes()
-	assert.Nil(t, err)
-	assert.Equal(t, []byte(`#include <testlib.h>`), sourceBytes)
-	sourceStr, err := item.GetSourceStr()
+func TestNew(t *testing.T) {
+	sketchFolderPath := paths.New("testdata", "SketchSimple")
+	mainFilePath := sketchFolderPath.Join(fmt.Sprintf("%s.ino", "SketchSimple"))
+	otherFile := sketchFolderPath.Join("other.cpp")
+
+	// Loading using Sketch folder path
+	sketch, err := New(sketchFolderPath)
 	assert.Nil(t, err)
-	assert.Equal(t, "#include <testlib.h>", sourceStr)
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.True(t, sketchFolderPath.EquivalentTo(sketch.FullPath))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	assert.Equal(t, sketch.AdditionalFiles.Len(), 1)
+	assert.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(otherFile))
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 1)
+	assert.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(otherFile))
 
-	item = NewItem("doesnt/exist")
-	sourceBytes, err = item.GetSourceBytes()
-	assert.Nil(t, sourceBytes)
-	assert.NotNil(t, err)
+	// Loading using Sketch main file path
+	sketch, err = New(mainFilePath)
+	assert.Nil(t, err)
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.True(t, sketchFolderPath.EquivalentTo(sketch.FullPath))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	assert.Equal(t, sketch.AdditionalFiles.Len(), 1)
+	assert.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(otherFile))
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 1)
+	assert.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(otherFile))
 }
 
-func TestSort(t *testing.T) {
-	items := []*Item{
-		{"foo"},
-		{"baz"},
-		{"bar"},
-	}
+func TestNewSketchPde(t *testing.T) {
+	sketchFolderPath := paths.New("testdata", "SketchPde")
+	mainFilePath := sketchFolderPath.Join(fmt.Sprintf("%s.pde", "SketchPde"))
 
-	sort.Sort(ItemByPath(items))
+	// Loading using Sketch folder path
+	sketch, err := New(sketchFolderPath)
+	assert.Nil(t, err)
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.True(t, sketchFolderPath.EquivalentTo(sketch.FullPath))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	assert.Equal(t, sketch.AdditionalFiles.Len(), 0)
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 0)
 
-	assert.Equal(t, "bar", items[0].Path)
-	assert.Equal(t, "baz", items[1].Path)
-	assert.Equal(t, "foo", items[2].Path)
+	// Loading using Sketch main file path
+	sketch, err = New(mainFilePath)
+	assert.Nil(t, err)
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.True(t, sketchFolderPath.EquivalentTo(sketch.FullPath))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	assert.Equal(t, sketch.AdditionalFiles.Len(), 0)
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 0)
 }
 
-func TestNew(t *testing.T) {
-	sketchFolderPath := filepath.Join("testdata", t.Name())
-	mainFilePath := filepath.Join(sketchFolderPath, t.Name()+".ino")
-	otherFile := filepath.Join(sketchFolderPath, "other.cpp")
-	allFilesPaths := []string{
-		mainFilePath,
-		otherFile,
-	}
-
-	sketch, err := New(sketchFolderPath, mainFilePath, "", allFilesPaths)
-	assert.Nil(t, err)
-	assert.Equal(t, mainFilePath, sketch.MainFile.Path)
-	assert.Equal(t, sketchFolderPath, sketch.LocationPath)
-	assert.Len(t, sketch.OtherSketchFiles, 0)
-	assert.Len(t, sketch.AdditionalFiles, 1)
-	assert.Equal(t, sketch.AdditionalFiles[0].Path, paths.New(sketchFolderPath).Join("other.cpp").String())
-	assert.Len(t, sketch.RootFolderFiles, 1)
-	assert.Equal(t, sketch.RootFolderFiles[0].Path, paths.New(sketchFolderPath).Join("other.cpp").String())
+func TestNewSketchBothInoAndPde(t *testing.T) {
+	sketchName := "SketchBothInoAndPde"
+	sketchFolderPath := paths.New("testdata", sketchName)
+	sketch, err := New(sketchFolderPath)
+	require.Nil(t, sketch)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "multiple main sketch files found")
+	require.Contains(t, err.Error(), fmt.Sprintf("%s.ino", sketchName))
+	require.Contains(t, err.Error(), fmt.Sprintf("%s.pde", sketchName))
+}
+
+func TestNewSketchWrongMain(t *testing.T) {
+	sketchName := "SketchWithWrongMain"
+	sketchFolderPath := paths.New("testdata", sketchName)
+	sketch, err := New(sketchFolderPath)
+	require.Nil(t, sketch)
+	require.Error(t, err)
+	sketchFolderPath, _ = sketchFolderPath.Abs()
+	expectedMainFile := sketchFolderPath.Join(sketchName)
+	expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolderPath, expectedMainFile)
+	require.Contains(t, err.Error(), expectedError)
+
+	sketchFolderPath = paths.New("testdata", sketchName)
+	mainFilePath := sketchFolderPath.Join(fmt.Sprintf("%s.ino", sketchName))
+	sketch, err = New(mainFilePath)
+	require.Nil(t, sketch)
+	require.Error(t, err)
+	sketchFolderPath, _ = sketchFolderPath.Abs()
+	expectedError = fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolderPath, expectedMainFile)
+	require.Contains(t, err.Error(), expectedError)
 }
 
 func TestNewSketchCasingWrong(t *testing.T) {
-	sketchPath := paths.New("testdata", "SketchCasingWrong")
-	mainFilePath := sketchPath.Join("sketchcasingwrong.ino").String()
-	sketch, err := New(sketchPath.String(), mainFilePath, "", []string{mainFilePath})
+	sketchPath := paths.New("testdata", "SketchWithWrongMain")
+	sketch, err := New(sketchPath)
 	assert.Nil(t, sketch)
 	assert.Error(t, err)
-	assert.IsType(t, &InvalidSketchFoldernameError{}, err)
-	e := err.(*InvalidSketchFoldernameError)
+	assert.IsType(t, &InvalidSketchFolderNameError{}, err)
+	e := err.(*InvalidSketchFolderNameError)
 	assert.NotNil(t, e.Sketch)
+	sketchPath, _ = sketchPath.Abs()
 	expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchPath.String(), sketchPath.Join(sketchPath.Base()+".ino"))
 	assert.EqualError(t, err, expectedError)
 }
 
 func TestNewSketchCasingCorrect(t *testing.T) {
 	sketchPath := paths.New("testdata", "SketchCasingCorrect")
-	mainFilePath := sketchPath.Join("SketchCasingCorrect.ino").String()
-	sketch, err := New(sketchPath.String(), mainFilePath, "", []string{mainFilePath})
+	mainFilePath := sketchPath.Join("SketchCasingCorrect.ino")
+	sketch, err := New(sketchPath)
 	assert.NotNil(t, sketch)
 	assert.NoError(t, err)
-	assert.Equal(t, sketchPath.String(), sketch.LocationPath)
-	assert.Equal(t, mainFilePath, sketch.MainFile.Path)
-	assert.Len(t, sketch.OtherSketchFiles, 0)
-	assert.Len(t, sketch.AdditionalFiles, 0)
-	assert.Len(t, sketch.RootFolderFiles, 0)
-}
-
-func TestCheckSketchCasingWrong(t *testing.T) {
-	sketchFolder := paths.New("testdata", "SketchCasingWrong")
-	err := CheckSketchCasing(sketchFolder.String())
-	expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolder, sketchFolder.Join(sketchFolder.Base()+".ino"))
-	assert.EqualError(t, err, expectedError)
-}
-
-func TestCheckSketchCasingCorrect(t *testing.T) {
-	sketchFolder := paths.New("testdata", "SketchCasingCorrect").String()
-	err := CheckSketchCasing(sketchFolder)
-	require.NoError(t, err)
+	assert.True(t, sketchPath.EquivalentTo(sketch.FullPath))
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	assert.Equal(t, sketch.AdditionalFiles.Len(), 0)
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 0)
 }
 
 func TestSketchWithMarkdownAsciidocJson(t *testing.T) {
 	sketchPath := paths.New("testdata", "SketchWithMarkdownAsciidocJson")
-	mainFilePath := sketchPath.Join("SketchWithMarkdownAsciidocJson.ino").String()
-	adocFilePath := sketchPath.Join("foo.adoc").String()
-	jsonFilePath := sketchPath.Join("foo.json").String()
-	mdFilePath := sketchPath.Join("foo.md").String()
+	mainFilePath := sketchPath.Join("SketchWithMarkdownAsciidocJson.ino")
+	adocFilePath := sketchPath.Join("foo.adoc")
+	jsonFilePath := sketchPath.Join("foo.json")
+	mdFilePath := sketchPath.Join("foo.md")
 
-	sketch, err := New(sketchPath.String(), mainFilePath, "", []string{mainFilePath, adocFilePath, jsonFilePath, mdFilePath})
+	sketch, err := New(sketchPath)
 	assert.NotNil(t, sketch)
 	assert.NoError(t, err)
-	assert.Equal(t, sketchPath.String(), sketch.LocationPath)
-	assert.Equal(t, mainFilePath, sketch.MainFile.Path)
-	assert.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 3)
-	require.Equal(t, "foo.adoc", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Equal(t, "foo.json", filepath.Base(sketch.AdditionalFiles[1].Path))
-	require.Equal(t, "foo.md", filepath.Base(sketch.AdditionalFiles[2].Path))
-	assert.Len(t, sketch.RootFolderFiles, 3)
-	require.Equal(t, "foo.adoc", filepath.Base(sketch.RootFolderFiles[0].Path))
-	require.Equal(t, "foo.json", filepath.Base(sketch.RootFolderFiles[1].Path))
-	require.Equal(t, "foo.md", filepath.Base(sketch.RootFolderFiles[2].Path))
+	assert.True(t, sketchPath.EquivalentTo(sketch.FullPath))
+	assert.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	assert.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	require.Equal(t, sketch.AdditionalFiles.Len(), 3)
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(adocFilePath))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(jsonFilePath))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(mdFilePath))
+	assert.Equal(t, sketch.RootFolderFiles.Len(), 3)
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(adocFilePath))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(jsonFilePath))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(mdFilePath))
 }
 
 func TestSketchWithTppFile(t *testing.T) {
 	sketchPath := paths.New("testdata", "SketchWithTppFile")
-	mainFilePath := sketchPath.Join("SketchWithTppFile.ino").String()
-	templateFile := sketchPath.Join("template.tpp").String()
+	mainFilePath := sketchPath.Join("SketchWithTppFile.ino")
+	templateFile := sketchPath.Join("template.tpp")
 
-	sketch, err := New(sketchPath.String(), mainFilePath, "", []string{mainFilePath, templateFile})
+	sketch, err := New(sketchPath)
 	require.NotNil(t, sketch)
 	require.NoError(t, err)
-	require.Equal(t, sketchPath.String(), sketch.LocationPath)
-	require.Equal(t, mainFilePath, sketch.MainFile.Path)
-	require.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 1)
-	require.Equal(t, "template.tpp", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Len(t, sketch.RootFolderFiles, 1)
-	require.Equal(t, "template.tpp", filepath.Base(sketch.RootFolderFiles[0].Path))
+	require.True(t, sketchPath.EquivalentTo(sketch.FullPath))
+	require.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	require.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	require.Equal(t, sketch.AdditionalFiles.Len(), 1)
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(templateFile))
+	require.Equal(t, sketch.RootFolderFiles.Len(), 1)
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(templateFile))
 }
 
 func TestSketchWithIppFile(t *testing.T) {
 	sketchPath := paths.New("testdata", "SketchWithIppFile")
-	mainFilePath := sketchPath.Join("SketchWithIppFile.ino").String()
-	templateFile := sketchPath.Join("template.ipp").String()
+	mainFilePath := sketchPath.Join("SketchWithIppFile.ino")
+	templateFile := sketchPath.Join("template.ipp")
 
-	sketch, err := New(sketchPath.String(), mainFilePath, "", []string{mainFilePath, templateFile})
+	sketch, err := New(sketchPath)
 	require.NotNil(t, sketch)
 	require.NoError(t, err)
-	require.Equal(t, sketchPath.String(), sketch.LocationPath)
-	require.Equal(t, mainFilePath, sketch.MainFile.Path)
-	require.Len(t, sketch.OtherSketchFiles, 0)
-	require.Len(t, sketch.AdditionalFiles, 1)
-	require.Equal(t, "template.ipp", filepath.Base(sketch.AdditionalFiles[0].Path))
-	require.Len(t, sketch.RootFolderFiles, 1)
-	require.Equal(t, "template.ipp", filepath.Base(sketch.RootFolderFiles[0].Path))
+	require.True(t, sketchPath.EquivalentTo(sketch.FullPath))
+	require.True(t, mainFilePath.EquivalentTo(sketch.MainFile))
+	require.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	require.Equal(t, sketch.AdditionalFiles.Len(), 1)
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(templateFile))
+	require.Equal(t, sketch.RootFolderFiles.Len(), 1)
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(templateFile))
+}
+
+func TestNewSketchFolderSymlink(t *testing.T) {
+	// pass the path to the sketch folder
+	sketchName := "SketchSymlink"
+	sketchPath, _ := paths.New("testdata", fmt.Sprintf("%sSrc", sketchName)).Abs()
+	sketchPathSymlink, _ := paths.New("testdata", sketchName).Abs()
+	os.Symlink(sketchPath.String(), sketchPathSymlink.String())
+	defer sketchPathSymlink.Remove()
+
+	mainFilePath := sketchPathSymlink.Join(fmt.Sprintf("%sSrc.ino", sketchName))
+	sketch, err := New(sketchPathSymlink)
+	require.Nil(t, err)
+	require.NotNil(t, sketch)
+	require.True(t, sketch.MainFile.EquivalentTo(mainFilePath))
+	require.True(t, sketch.FullPath.EquivalentTo(sketchPath))
+	require.True(t, sketch.FullPath.EquivalentTo(sketchPathSymlink))
+	require.Equal(t, sketch.OtherSketchFiles.Len(), 2)
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPath.Join("old.pde")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPath.Join("other.ino")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPathSymlink.Join("old.pde")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPathSymlink.Join("other.ino")))
+	require.Equal(t, sketch.AdditionalFiles.Len(), 3)
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("header.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("s_file.S")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("src", "helper.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("header.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("s_file.S")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("src", "helper.h")))
+	require.Equal(t, sketch.RootFolderFiles.Len(), 4)
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("header.h")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("old.pde")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("other.ino")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("s_file.S")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("header.h")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("old.pde")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("other.ino")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("s_file.S")))
+
+	// pass the path to the main file
+	sketch, err = New(mainFilePath)
+	require.Nil(t, err)
+	require.NotNil(t, sketch)
+	require.True(t, sketch.MainFile.EquivalentTo(mainFilePath))
+	require.True(t, sketch.FullPath.EquivalentTo(sketchPath))
+	require.True(t, sketch.FullPath.EquivalentTo(sketchPathSymlink))
+	require.Equal(t, sketch.OtherSketchFiles.Len(), 2)
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPath.Join("old.pde")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPath.Join("other.ino")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPathSymlink.Join("old.pde")))
+	require.True(t, sketch.OtherSketchFiles.ContainsEquivalentTo(sketchPathSymlink.Join("other.ino")))
+	require.Equal(t, sketch.AdditionalFiles.Len(), 3)
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("header.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("s_file.S")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPath.Join("src", "helper.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("header.h")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("s_file.S")))
+	require.True(t, sketch.AdditionalFiles.ContainsEquivalentTo(sketchPathSymlink.Join("src", "helper.h")))
+	require.Equal(t, sketch.RootFolderFiles.Len(), 4)
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("header.h")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("old.pde")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("other.ino")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPath.Join("s_file.S")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("header.h")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("old.pde")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("other.ino")))
+	require.True(t, sketch.RootFolderFiles.ContainsEquivalentTo(sketchPathSymlink.Join("s_file.S")))
+}
+
+func TestGenBuildPath(t *testing.T) {
+	want := paths.TempDir().Join("arduino-sketch-ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
+	assert.True(t, GenBuildPath(paths.New("foo")).EquivalentTo(want))
+
+	want = paths.TempDir().Join("arduino-sketch-D41D8CD98F00B204E9800998ECF8427E")
+	assert.True(t, GenBuildPath(nil).EquivalentTo(want))
+}
+
+func TestCheckForPdeFiles(t *testing.T) {
+	sketchPath := paths.New("testdata", "SketchSimple")
+	files := CheckForPdeFiles(sketchPath)
+	require.Empty(t, files)
+
+	sketchPath = paths.New("testdata", "SketchPde")
+	files = CheckForPdeFiles(sketchPath)
+	require.Len(t, files, 1)
+	require.Equal(t, sketchPath.Join("SketchPde.pde"), files[0])
+
+	sketchPath = paths.New("testdata", "SketchMultipleMainFiles")
+	files = CheckForPdeFiles(sketchPath)
+	require.Len(t, files, 1)
+	require.Equal(t, sketchPath.Join("SketchMultipleMainFiles.pde"), files[0])
+
+	sketchPath = paths.New("testdata", "SketchSimple", "SketchSimple.ino")
+	files = CheckForPdeFiles(sketchPath)
+	require.Empty(t, files)
+
+	sketchPath = paths.New("testdata", "SketchPde", "SketchPde.pde")
+	files = CheckForPdeFiles(sketchPath)
+	require.Len(t, files, 1)
+	require.Equal(t, sketchPath.Parent().Join("SketchPde.pde"), files[0])
+
+	sketchPath = paths.New("testdata", "SketchMultipleMainFiles", "SketchMultipleMainFiles.ino")
+	files = CheckForPdeFiles(sketchPath)
+	require.Len(t, files, 1)
+	require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
+
+	sketchPath = paths.New("testdata", "SketchMultipleMainFiles", "SketchMultipleMainFiles.pde")
+	files = CheckForPdeFiles(sketchPath)
+	require.Len(t, files, 1)
+	require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
 }
diff --git a/arduino/sketches/testdata/SketchCasingCorrect/SketchCasingCorrect.ino b/arduino/sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.ino
similarity index 100%
rename from arduino/sketches/testdata/SketchCasingCorrect/SketchCasingCorrect.ino
rename to arduino/sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.ino
diff --git a/arduino/sketches/testdata/SketchCasingWrong/sketchcasingwrong.ino b/arduino/sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.pde
similarity index 100%
rename from arduino/sketches/testdata/SketchCasingWrong/sketchcasingwrong.ino
rename to arduino/sketch/testdata/SketchBothInoAndPde/SketchBothInoAndPde.pde
diff --git a/arduino/sketches/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.ino b/arduino/sketch/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.ino
similarity index 100%
rename from arduino/sketches/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.ino
rename to arduino/sketch/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.ino
diff --git a/arduino/sketches/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.pde b/arduino/sketch/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.pde
similarity index 100%
rename from arduino/sketches/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.pde
rename to arduino/sketch/testdata/SketchMultipleMainFiles/SketchMultipleMainFiles.pde
diff --git a/arduino/sketches/testdata/SketchPde/SketchPde.pde b/arduino/sketch/testdata/SketchPde/SketchPde.pde
similarity index 100%
rename from arduino/sketches/testdata/SketchPde/SketchPde.pde
rename to arduino/sketch/testdata/SketchPde/SketchPde.pde
diff --git a/arduino/sketch/testdata/TestNew/TestNew.ino b/arduino/sketch/testdata/SketchSimple/SketchSimple.ino
similarity index 100%
rename from arduino/sketch/testdata/TestNew/TestNew.ino
rename to arduino/sketch/testdata/SketchSimple/SketchSimple.ino
diff --git a/arduino/sketch/testdata/TestNew/other.cpp b/arduino/sketch/testdata/SketchSimple/other.cpp
similarity index 100%
rename from arduino/sketch/testdata/TestNew/other.cpp
rename to arduino/sketch/testdata/SketchSimple/other.cpp
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/.#sketch.ino b/arduino/sketch/testdata/SketchSymlinkSrc/.#sketch.ino
new file mode 100644
index 00000000000..71048175432
--- /dev/null
+++ b/arduino/sketch/testdata/SketchSymlinkSrc/.#sketch.ino
@@ -0,0 +1,2 @@
+void setup()
+void loop) }
\ No newline at end of file
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/SketchSymlinkSrc.ino b/arduino/sketch/testdata/SketchSymlinkSrc/SketchSymlinkSrc.ino
new file mode 100644
index 00000000000..32f56baab79
--- /dev/null
+++ b/arduino/sketch/testdata/SketchSymlinkSrc/SketchSymlinkSrc.ino
@@ -0,0 +1,7 @@
+void setup() {
+
+}
+
+void loop() {
+
+}
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/doc.txt b/arduino/sketch/testdata/SketchSymlinkSrc/doc.txt
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/header.h b/arduino/sketch/testdata/SketchSymlinkSrc/header.h
new file mode 100644
index 00000000000..0e7d3b1a6a9
--- /dev/null
+++ b/arduino/sketch/testdata/SketchSymlinkSrc/header.h
@@ -0,0 +1 @@
+#define FOO "BAR"
\ No newline at end of file
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/old.pde b/arduino/sketch/testdata/SketchSymlinkSrc/old.pde
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/other.ino b/arduino/sketch/testdata/SketchSymlinkSrc/other.ino
new file mode 100644
index 00000000000..c426196c017
--- /dev/null
+++ b/arduino/sketch/testdata/SketchSymlinkSrc/other.ino
@@ -0,0 +1,3 @@
+String hello() {
+  return "world";
+}
\ No newline at end of file
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/s_file.S b/arduino/sketch/testdata/SketchSymlinkSrc/s_file.S
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/src/dont_load_me.ino b/arduino/sketch/testdata/SketchSymlinkSrc/src/dont_load_me.ino
new file mode 100644
index 00000000000..46b07018d09
--- /dev/null
+++ b/arduino/sketch/testdata/SketchSymlinkSrc/src/dont_load_me.ino
@@ -0,0 +1,2 @@
+#include <testlib4.h>
+#error "Whattya looking at?"
diff --git a/arduino/sketch/testdata/SketchSymlinkSrc/src/helper.h b/arduino/sketch/testdata/SketchSymlinkSrc/src/helper.h
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/arduino/sketches/testdata/Sketch1/Sketch1.ino b/arduino/sketch/testdata/SketchWithWrongMain/main.ino
similarity index 96%
rename from arduino/sketches/testdata/Sketch1/Sketch1.ino
rename to arduino/sketch/testdata/SketchWithWrongMain/main.ino
index 5054c040393..660bdbccfdb 100644
--- a/arduino/sketches/testdata/Sketch1/Sketch1.ino
+++ b/arduino/sketch/testdata/SketchWithWrongMain/main.ino
@@ -1,3 +1,2 @@
-
 void setup() {}
 void loop() {}
diff --git a/arduino/sketches/sketches.go b/arduino/sketches/sketches.go
deleted file mode 100644
index bc15dfb9762..00000000000
--- a/arduino/sketches/sketches.go
+++ /dev/null
@@ -1,148 +0,0 @@
-// This file is part of arduino-cli.
-//
-// Copyright 2020 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 sketches
-
-import (
-	"encoding/json"
-	"fmt"
-
-	"github.com/arduino/arduino-cli/arduino/builder"
-	"github.com/arduino/arduino-cli/arduino/globals"
-	"github.com/arduino/arduino-cli/arduino/sketch"
-	"github.com/arduino/go-paths-helper"
-	"github.com/pkg/errors"
-)
-
-// Sketch is a sketch for Arduino
-type Sketch struct {
-	Name              string
-	MainFileExtension string
-	FullPath          *paths.Path
-	Metadata          *Metadata
-}
-
-// Metadata is the kind of data associated to a project such as the connected board
-type Metadata struct {
-	CPU BoardMetadata `json:"cpu,omitempty" gorethink:"cpu"`
-}
-
-// BoardMetadata represents the board metadata for the sketch
-type BoardMetadata struct {
-	Fqbn string `json:"fqbn,required"`
-	Name string `json:"name,omitempty"`
-	Port string `json:"port,omitepty"`
-}
-
-// NewSketchFromPath loads a sketch from the specified path
-func NewSketchFromPath(path *paths.Path) (*Sketch, error) {
-	path, err := path.Abs()
-	if err != nil {
-		return nil, errors.Errorf("getting sketch path: %s", err)
-	}
-	if !path.IsDir() {
-		path = path.Parent()
-	}
-
-	var mainSketchFile *paths.Path
-	for ext := range globals.MainFileValidExtensions {
-		candidateSketchMainFile := path.Join(path.Base() + ext)
-		if candidateSketchMainFile.Exist() {
-			if mainSketchFile == nil {
-				mainSketchFile = candidateSketchMainFile
-			} else {
-				return nil, errors.Errorf("multiple main sketch files found (%v, %v)",
-					mainSketchFile,
-					candidateSketchMainFile,
-				)
-			}
-		}
-	}
-
-	if mainSketchFile == nil || sketch.CheckSketchCasing(path.String()) != nil {
-		sketchFile := path.Join(path.Base() + globals.MainFileValidExtension)
-		return nil, errors.Errorf("no valid sketch found in %s: missing %s", path, sketchFile)
-	}
-
-	s := &Sketch{
-		FullPath:          path,
-		MainFileExtension: mainSketchFile.Ext(),
-		Name:              path.Base(),
-		Metadata:          &Metadata{},
-	}
-	s.ImportMetadata()
-	return s, nil
-}
-
-// ImportMetadata imports metadata into the sketch from a sketch.json file in the root
-// path of the sketch.
-func (s *Sketch) ImportMetadata() error {
-	sketchJSON := s.FullPath.Join("sketch.json")
-	content, err := sketchJSON.ReadFile()
-	if err != nil {
-		return fmt.Errorf("reading sketch metadata %s: %s", sketchJSON, err)
-	}
-	var meta Metadata
-	err = json.Unmarshal(content, &meta)
-	if err != nil {
-		if s.Metadata == nil {
-			s.Metadata = new(Metadata)
-		}
-		return fmt.Errorf("encoding sketch metadata: %s", err)
-	}
-	s.Metadata = &meta
-	return nil
-}
-
-// ExportMetadata writes sketch metadata into a sketch.json file in the root path of
-// the sketch
-func (s *Sketch) ExportMetadata() error {
-	d, err := json.MarshalIndent(&s.Metadata, "", "  ")
-	if err != nil {
-		return fmt.Errorf("decoding sketch metadata: %s", err)
-	}
-
-	sketchJSON := s.FullPath.Join("sketch.json")
-	if err := sketchJSON.WriteFile(d); err != nil {
-		return fmt.Errorf("writing sketch metadata %s: %s", sketchJSON, err)
-	}
-	return nil
-}
-
-// BuildPath returns this Sketch build path in the temp directory of the system.
-// Returns an error if the Sketch's FullPath is not set
-func (s *Sketch) BuildPath() (*paths.Path, error) {
-	if s.FullPath == nil {
-		return nil, fmt.Errorf("sketch path is empty")
-	}
-	return builder.GenBuildPath(s.FullPath), nil
-}
-
-// CheckForPdeFiles returns all files ending with .pde extension
-// in dir, this is mainly used to warn the user that these files
-// must be changed to .ino extension.
-// When .pde files won't be supported anymore this function must be removed.
-func CheckForPdeFiles(sketch *paths.Path) []*paths.Path {
-	if sketch.IsNotDir() {
-		sketch = sketch.Parent()
-	}
-
-	files, err := sketch.ReadDirRecursive()
-	if err != nil {
-		return []*paths.Path{}
-	}
-	files.FilterSuffix(".pde")
-	return files
-}
diff --git a/arduino/sketches/sketches_test.go b/arduino/sketches/sketches_test.go
deleted file mode 100644
index ad65df39c0b..00000000000
--- a/arduino/sketches/sketches_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-// This file is part of arduino-cli.
-//
-// Copyright 2020 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 sketches
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/arduino/go-paths-helper"
-	"github.com/stretchr/testify/require"
-)
-
-func TestSketchLoadingFromFolderOrMainFile(t *testing.T) {
-	skFolder := paths.New("testdata/Sketch1")
-	skMainIno := skFolder.Join("Sketch1.ino")
-
-	{
-		sk, err := NewSketchFromPath(skFolder)
-		require.NoError(t, err)
-		require.Equal(t, sk.Name, "Sketch1")
-		fmt.Println(sk.FullPath.String(), "==", skFolder.String())
-		require.True(t, sk.FullPath.EquivalentTo(skFolder))
-	}
-
-	{
-		sk, err := NewSketchFromPath(skMainIno)
-		require.NoError(t, err)
-		require.Equal(t, sk.Name, "Sketch1")
-		fmt.Println(sk.FullPath.String(), "==", skFolder.String())
-		require.True(t, sk.FullPath.EquivalentTo(skFolder))
-	}
-}
-
-func TestSketchBuildPath(t *testing.T) {
-	// Verifies build path is returned if sketch path is set
-	sketchPath := paths.New("testdata/Sketch1")
-	sketch, err := NewSketchFromPath(sketchPath)
-	require.NoError(t, err)
-	buildPath, err := sketch.BuildPath()
-	require.NoError(t, err)
-	require.Contains(t, buildPath.String(), "arduino-sketch-")
-
-	// Verifies sketch path is returned if sketch has .pde extension
-	sketchPath = paths.New("testdata", "SketchPde")
-	sketch, err = NewSketchFromPath(sketchPath)
-	require.NoError(t, err)
-	require.NotNil(t, sketch)
-	buildPath, err = sketch.BuildPath()
-	require.NoError(t, err)
-	require.Contains(t, buildPath.String(), "arduino-sketch-")
-
-	// Verifies error is returned if there are multiple main files
-	sketchPath = paths.New("testdata", "SketchMultipleMainFiles")
-	sketch, err = NewSketchFromPath(sketchPath)
-	require.Nil(t, sketch)
-	require.Error(t, err, "multiple main sketch files found")
-
-	// Verifies error is returned if sketch path is not set
-	sketch = &Sketch{}
-	buildPath, err = sketch.BuildPath()
-	require.Nil(t, buildPath)
-	require.Error(t, err, "sketch path is empty")
-}
-
-func TestCheckForPdeFiles(t *testing.T) {
-	sketchPath := paths.New("testdata", "Sketch1")
-	files := CheckForPdeFiles(sketchPath)
-	require.Empty(t, files)
-
-	sketchPath = paths.New("testdata", "SketchPde")
-	files = CheckForPdeFiles(sketchPath)
-	require.Len(t, files, 1)
-	require.Equal(t, sketchPath.Join("SketchPde.pde"), files[0])
-
-	sketchPath = paths.New("testdata", "SketchMultipleMainFiles")
-	files = CheckForPdeFiles(sketchPath)
-	require.Len(t, files, 1)
-	require.Equal(t, sketchPath.Join("SketchMultipleMainFiles.pde"), files[0])
-
-	sketchPath = paths.New("testdata", "Sketch1", "Sketch1.ino")
-	files = CheckForPdeFiles(sketchPath)
-	require.Empty(t, files)
-
-	sketchPath = paths.New("testdata", "SketchPde", "SketchPde.pde")
-	files = CheckForPdeFiles(sketchPath)
-	require.Len(t, files, 1)
-	require.Equal(t, sketchPath.Parent().Join("SketchPde.pde"), files[0])
-
-	sketchPath = paths.New("testdata", "SketchMultipleMainFiles", "SketchMultipleMainFiles.ino")
-	files = CheckForPdeFiles(sketchPath)
-	require.Len(t, files, 1)
-	require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
-
-	sketchPath = paths.New("testdata", "SketchMultipleMainFiles", "SketchMultipleMainFiles.pde")
-	files = CheckForPdeFiles(sketchPath)
-	require.Len(t, files, 1)
-	require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
-}
-
-func TestSketchLoadWithCasing(t *testing.T) {
-	sketchFolder := paths.New("testdata", "SketchCasingWrong")
-
-	sketch, err := NewSketchFromPath(sketchFolder)
-	require.Nil(t, sketch)
-
-	sketchFolderAbs, _ := sketchFolder.Abs()
-	sketchMainFileAbs := sketchFolderAbs.Join("SketchCasingWrong.ino")
-	expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolderAbs, sketchMainFileAbs)
-	require.EqualError(t, err, expectedError)
-}
-
-func TestSketchLoadingCorrectCasing(t *testing.T) {
-	sketchFolder := paths.New("testdata", "SketchCasingCorrect")
-	sketch, err := NewSketchFromPath(sketchFolder)
-	require.NotNil(t, sketch)
-	require.NoError(t, err)
-	require.Equal(t, sketch.Name, "SketchCasingCorrect")
-	require.True(t, sketch.FullPath.EquivalentTo(sketchFolder))
-}
diff --git a/cli/compile/compile.go b/cli/compile/compile.go
index 73f479507be..eac43f41150 100644
--- a/cli/compile/compile.go
+++ b/cli/compile/compile.go
@@ -21,7 +21,7 @@ import (
 	"encoding/json"
 	"os"
 
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/cli/feedback"
 	"github.com/arduino/arduino-cli/cli/output"
 	"github.com/arduino/arduino-cli/configuration"
@@ -130,7 +130,7 @@ func run(cmd *cobra.Command, args []string) {
 	sketchPath := initSketchPath(path)
 
 	// .pde files are still supported but deprecated, this warning urges the user to rename them
-	if files := sketches.CheckForPdeFiles(sketchPath); len(files) > 0 {
+	if files := sketch.CheckForPdeFiles(sketchPath); len(files) > 0 {
 		feedback.Error("Sketches with .pde extension are deprecated, please rename the following files to .ino:")
 		for _, f := range files {
 			feedback.Error(f)
diff --git a/cli/sketch/archive.go b/cli/sketch/archive.go
index 940f0e6c0fc..4b7c07b03dc 100644
--- a/cli/sketch/archive.go
+++ b/cli/sketch/archive.go
@@ -19,10 +19,10 @@ import (
 	"context"
 	"os"
 
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/cli/errorcodes"
 	"github.com/arduino/arduino-cli/cli/feedback"
-	"github.com/arduino/arduino-cli/commands/sketch"
+	sk "github.com/arduino/arduino-cli/commands/sketch"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
@@ -55,13 +55,13 @@ func initArchiveCommand() *cobra.Command {
 func runArchiveCommand(cmd *cobra.Command, args []string) {
 	logrus.Info("Executing `arduino sketch archive`")
 
-	sketchPath := "."
+	sketchPath := paths.New(".")
 	if len(args) >= 1 {
-		sketchPath = args[0]
+		sketchPath = paths.New(args[0])
 	}
 
 	// .pde files are still supported but deprecated, this warning urges the user to rename them
-	if files := sketches.CheckForPdeFiles(paths.New(sketchPath)); len(files) > 0 {
+	if files := sketch.CheckForPdeFiles(sketchPath); len(files) > 0 {
 		feedback.Error("Sketches with .pde extension are deprecated, please rename the following files to .ino:")
 		for _, f := range files {
 			feedback.Error(f)
@@ -73,9 +73,9 @@ func runArchiveCommand(cmd *cobra.Command, args []string) {
 		archivePath = args[1]
 	}
 
-	_, err := sketch.ArchiveSketch(context.Background(),
+	_, err := sk.ArchiveSketch(context.Background(),
 		&rpc.ArchiveSketchRequest{
-			SketchPath:      sketchPath,
+			SketchPath:      sketchPath.String(),
 			ArchivePath:     archivePath,
 			IncludeBuildDir: includeBuildDir,
 		})
diff --git a/cli/upload/upload.go b/cli/upload/upload.go
index 9a89c4efd9f..7d0f69f6b52 100644
--- a/cli/upload/upload.go
+++ b/cli/upload/upload.go
@@ -19,7 +19,7 @@ import (
 	"context"
 	"os"
 
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/cli/errorcodes"
 	"github.com/arduino/arduino-cli/cli/feedback"
 	"github.com/arduino/arduino-cli/cli/instance"
@@ -82,7 +82,7 @@ func run(command *cobra.Command, args []string) {
 	sketchPath := initSketchPath(path)
 
 	// .pde files are still supported but deprecated, this warning urges the user to rename them
-	if files := sketches.CheckForPdeFiles(sketchPath); len(files) > 0 {
+	if files := sketch.CheckForPdeFiles(sketchPath); len(files) > 0 {
 		feedback.Error("Sketches with .pde extension are deprecated, please rename the following files to .ino:")
 		for _, f := range files {
 			feedback.Error(f)
diff --git a/commands/board/attach.go b/commands/board/attach.go
index f5bd4017eaa..32b8e2e53a8 100644
--- a/commands/board/attach.go
+++ b/commands/board/attach.go
@@ -25,7 +25,7 @@ import (
 
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/commands"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	discovery "github.com/arduino/board-discovery"
@@ -42,7 +42,7 @@ func Attach(ctx context.Context, req *rpc.BoardAttachRequest, taskCB commands.Ta
 	if req.GetSketchPath() != "" {
 		sketchPath = paths.New(req.GetSketchPath())
 	}
-	sketch, err := sketches.NewSketchFromPath(sketchPath)
+	sk, err := sketch.New(sketchPath)
 	if err != nil {
 		return nil, fmt.Errorf("opening sketch: %s", err)
 	}
@@ -54,7 +54,7 @@ func Attach(ctx context.Context, req *rpc.BoardAttachRequest, taskCB commands.Ta
 	}
 
 	if fqbn != nil {
-		sketch.Metadata.CPU = sketches.BoardMetadata{
+		sk.Metadata.CPU = sketch.BoardMetadata{
 			Fqbn: fqbn.String(),
 		}
 	} else {
@@ -92,18 +92,18 @@ func Attach(ctx context.Context, req *rpc.BoardAttachRequest, taskCB commands.Ta
 
 		// TODO: should be stoped the monitor: when running as a pure CLI  is released
 		// by the OS, when run as daemon the resource's state is unknown and could be leaked.
-		sketch.Metadata.CPU = sketches.BoardMetadata{
+		sk.Metadata.CPU = sketch.BoardMetadata{
 			Fqbn: board.FQBN(),
 			Name: board.Name(),
 			Port: deviceURI.String(),
 		}
 	}
 
-	err = sketch.ExportMetadata()
+	err = sk.ExportMetadata()
 	if err != nil {
 		return nil, fmt.Errorf("cannot export sketch metadata: %s", err)
 	}
-	taskCB(&rpc.TaskProgress{Name: "Selected fqbn: " + sketch.Metadata.CPU.Fqbn, Completed: true})
+	taskCB(&rpc.TaskProgress{Name: "Selected fqbn: " + sk.Metadata.CPU.Fqbn, Completed: true})
 	return &rpc.BoardAttachResponse{}, nil
 }
 
diff --git a/commands/compile/compile.go b/commands/compile/compile.go
index 79d8a6b6fc9..4a884e600ca 100644
--- a/commands/compile/compile.go
+++ b/commands/compile/compile.go
@@ -27,7 +27,7 @@ import (
 	bldr "github.com/arduino/arduino-cli/arduino/builder"
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/commands"
 	"github.com/arduino/arduino-cli/configuration"
 	"github.com/arduino/arduino-cli/legacy/builder"
@@ -95,14 +95,14 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
 		return nil, fmt.Errorf("missing sketchPath")
 	}
 	sketchPath := paths.New(req.GetSketchPath())
-	sketch, err := sketches.NewSketchFromPath(sketchPath)
+	sk, err := sketch.New(sketchPath)
 	if err != nil {
 		return nil, fmt.Errorf("opening sketch: %s", err)
 	}
 
 	fqbnIn := req.GetFqbn()
-	if fqbnIn == "" && sketch != nil && sketch.Metadata != nil {
-		fqbnIn = sketch.Metadata.CPU.Fqbn
+	if fqbnIn == "" && sk != nil && sk.Metadata != nil {
+		fqbnIn = sk.Metadata.CPU.Fqbn
 	}
 	if fqbnIn == "" {
 		return nil, fmt.Errorf("no FQBN provided")
@@ -128,7 +128,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
 	builderCtx := &types.Context{}
 	builderCtx.PackageManager = pm
 	builderCtx.FQBN = fqbn
-	builderCtx.SketchLocation = sketch.FullPath
+	builderCtx.SketchLocation = sk.FullPath
 
 	// FIXME: This will be redundant when arduino-builder will be part of the cli
 	builderCtx.HardwareDirs = configuration.HardwareDirectories(configuration.Settings)
@@ -140,7 +140,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
 	builderCtx.LibraryDirs = paths.NewPathList(req.Library...)
 
 	if req.GetBuildPath() == "" {
-		builderCtx.BuildPath = bldr.GenBuildPath(sketch.FullPath)
+		builderCtx.BuildPath = sk.BuildPath
 	} else {
 		builderCtx.BuildPath = paths.New(req.GetBuildPath())
 	}
@@ -243,7 +243,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
 		} else {
 			// Add FQBN (without configs part) to export path
 			fqbnSuffix := strings.Replace(fqbn.StringWithoutConfig(), ":", ".", -1)
-			exportPath = sketch.FullPath.Join("build", fqbnSuffix)
+			exportPath = sk.FullPath.Join("build", fqbnSuffix)
 		}
 		logrus.WithField("path", exportPath).Trace("Saving sketch to export path.")
 		if err := exportPath.MkdirAll(); err != nil {
@@ -281,7 +281,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
 		importedLibs = append(importedLibs, rpcLib)
 	}
 
-	logrus.Tracef("Compile %s for %s successful", sketch.Name, fqbnIn)
+	logrus.Tracef("Compile %s for %s successful", sk.Name, fqbnIn)
 
 	return &rpc.CompileResponse{
 		UsedLibraries:          importedLibs,
diff --git a/commands/debug/debug_info.go b/commands/debug/debug_info.go
index 76b39145f3b..d3bdf949b02 100644
--- a/commands/debug/debug_info.go
+++ b/commands/debug/debug_info.go
@@ -22,7 +22,7 @@ import (
 
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/commands"
 	"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/debug/v1"
 	"github.com/arduino/go-paths-helper"
@@ -47,15 +47,15 @@ func getDebugProperties(req *debug.DebugConfigRequest, pm *packagemanager.Packag
 		return nil, fmt.Errorf("missing sketchPath")
 	}
 	sketchPath := paths.New(req.GetSketchPath())
-	sketch, err := sketches.NewSketchFromPath(sketchPath)
+	sk, err := sketch.New(sketchPath)
 	if err != nil {
 		return nil, errors.Wrap(err, "opening sketch")
 	}
 
 	// XXX Remove this code duplication!!
 	fqbnIn := req.GetFqbn()
-	if fqbnIn == "" && sketch != nil && sketch.Metadata != nil {
-		fqbnIn = sketch.Metadata.CPU.Fqbn
+	if fqbnIn == "" && sk != nil && sk.Metadata != nil {
+		fqbnIn = sk.Metadata.CPU.Fqbn
 	}
 	if fqbnIn == "" {
 		return nil, fmt.Errorf("no Fully Qualified Board Name provided")
@@ -115,15 +115,9 @@ func getDebugProperties(req *debug.DebugConfigRequest, pm *packagemanager.Packag
 		}
 	}
 
-	var importPath *paths.Path
+	importPath := sk.BuildPath
 	if importDir := req.GetImportDir(); importDir != "" {
 		importPath = paths.New(importDir)
-	} else {
-		// TODO: Create a function to obtain importPath from sketch
-		importPath, err = sketch.BuildPath()
-		if err != nil {
-			return nil, fmt.Errorf("can't find build path for sketch: %v", err)
-		}
 	}
 	if !importPath.Exist() {
 		return nil, fmt.Errorf("compiled sketch not found in %s", importPath)
@@ -132,7 +126,7 @@ func getDebugProperties(req *debug.DebugConfigRequest, pm *packagemanager.Packag
 		return nil, fmt.Errorf("expected compiled sketch in directory %s, but is a file instead", importPath)
 	}
 	toolProperties.SetPath("build.path", importPath)
-	toolProperties.Set("build.project_name", sketch.Name+".ino")
+	toolProperties.Set("build.project_name", sk.Name+".ino")
 
 	// Set debug port property
 	port := req.GetPort()
diff --git a/commands/instances.go b/commands/instances.go
index c2657daac9d..28a541a2285 100644
--- a/commands/instances.go
+++ b/commands/instances.go
@@ -23,7 +23,6 @@ import (
 	"os"
 	"path"
 
-	"github.com/arduino/arduino-cli/arduino/builder"
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packageindex"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
@@ -31,6 +30,7 @@ import (
 	"github.com/arduino/arduino-cli/arduino/libraries/librariesindex"
 	"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
 	"github.com/arduino/arduino-cli/arduino/security"
+	sk "github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/arduino/utils"
 	"github.com/arduino/arduino-cli/cli/globals"
 	"github.com/arduino/arduino-cli/configuration"
@@ -842,29 +842,30 @@ func Upgrade(ctx context.Context, req *rpc.UpgradeRequest, downloadCB DownloadPr
 
 // LoadSketch collects and returns all files composing a sketch
 func LoadSketch(ctx context.Context, req *rpc.LoadSketchRequest) (*rpc.LoadSketchResponse, error) {
-	sketch, err := builder.SketchLoad(req.SketchPath, "")
+	// TODO: This a ToRpc function for the Sketch struct
+	sketch, err := sk.New(paths.New(req.SketchPath))
 	if err != nil {
-		return nil, fmt.Errorf("Error loading sketch %v: %v", req.SketchPath, err)
+		return nil, fmt.Errorf("error loading sketch %v: %v", req.SketchPath, err)
 	}
 
-	otherSketchFiles := make([]string, len(sketch.OtherSketchFiles))
-	for i, file := range sketch.OtherSketchFiles {
-		otherSketchFiles[i] = file.Path
+	otherSketchFiles := make([]string, sketch.OtherSketchFiles.Len())
+	for i, file := range *sketch.OtherSketchFiles {
+		otherSketchFiles[i] = file.String()
 	}
 
-	additionalFiles := make([]string, len(sketch.AdditionalFiles))
-	for i, file := range sketch.AdditionalFiles {
-		additionalFiles[i] = file.Path
+	additionalFiles := make([]string, sketch.AdditionalFiles.Len())
+	for i, file := range *sketch.AdditionalFiles {
+		additionalFiles[i] = file.String()
 	}
 
-	rootFolderFiles := make([]string, len(sketch.RootFolderFiles))
-	for i, file := range sketch.RootFolderFiles {
-		rootFolderFiles[i] = file.Path
+	rootFolderFiles := make([]string, sketch.RootFolderFiles.Len())
+	for i, file := range *sketch.RootFolderFiles {
+		rootFolderFiles[i] = file.String()
 	}
 
 	return &rpc.LoadSketchResponse{
-		MainFile:         sketch.MainFile.Path,
-		LocationPath:     sketch.LocationPath,
+		MainFile:         sketch.MainFile.String(),
+		LocationPath:     sketch.FullPath.String(),
 		OtherSketchFiles: otherSketchFiles,
 		AdditionalFiles:  additionalFiles,
 		RootFolderFiles:  rootFolderFiles,
diff --git a/commands/sketch/archive.go b/commands/sketch/archive.go
index a388eee61dc..79e58973068 100644
--- a/commands/sketch/archive.go
+++ b/commands/sketch/archive.go
@@ -23,7 +23,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
 )
@@ -38,13 +38,13 @@ func ArchiveSketch(ctx context.Context, req *rpc.ArchiveSketchRequest) (*rpc.Arc
 		sketchPath = paths.New(".")
 	}
 
-	sketch, err := sketches.NewSketchFromPath(sketchPath)
+	s, err := sketch.New(sketchPath)
 	if err != nil {
 		return nil, err
 	}
 
-	sketchPath = sketch.FullPath
-	sketchName = sketch.Name
+	sketchPath = s.FullPath
+	sketchName = s.Name
 
 	archivePath := paths.New(req.ArchivePath)
 	if archivePath == nil {
diff --git a/commands/upload/upload.go b/commands/upload/upload.go
index 0299d716fc7..859c9489860 100644
--- a/commands/upload/upload.go
+++ b/commands/upload/upload.go
@@ -23,12 +23,11 @@ import (
 	"path/filepath"
 	"strings"
 
-	bldr "github.com/arduino/arduino-cli/arduino/builder"
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
 	"github.com/arduino/arduino-cli/arduino/globals"
 	"github.com/arduino/arduino-cli/arduino/serialutils"
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/commands"
 	"github.com/arduino/arduino-cli/executils"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
@@ -45,7 +44,7 @@ func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, er
 	// TODO: make a generic function to extract sketch from request
 	// and remove duplication in commands/compile.go
 	sketchPath := paths.New(req.GetSketchPath())
-	sketch, err := sketches.NewSketchFromPath(sketchPath)
+	sk, err := sketch.New(sketchPath)
 	if err != nil && req.GetImportDir() == "" && req.GetImportFile() == "" {
 		return nil, fmt.Errorf("opening sketch: %s", err)
 	}
@@ -54,7 +53,7 @@ func Upload(ctx context.Context, req *rpc.UploadRequest, outStream io.Writer, er
 
 	err = runProgramAction(
 		pm,
-		sketch,
+		sk,
 		req.GetImportFile(),
 		req.GetImportDir(),
 		req.GetFqbn(),
@@ -95,7 +94,7 @@ func UsingProgrammer(ctx context.Context, req *rpc.UploadUsingProgrammerRequest,
 }
 
 func runProgramAction(pm *packagemanager.PackageManager,
-	sketch *sketches.Sketch,
+	sk *sketch.Sketch,
 	importFile, importDir, fqbnIn, port string,
 	programmerID string,
 	verbose, verify, burnBootloader bool,
@@ -107,8 +106,8 @@ func runProgramAction(pm *packagemanager.PackageManager,
 	}
 
 	// FIXME: make a specification on how a port is specified via command line
-	if port == "" && sketch != nil && sketch.Metadata != nil {
-		deviceURI, err := url.Parse(sketch.Metadata.CPU.Port)
+	if port == "" && sk != nil && sk.Metadata != nil {
+		deviceURI, err := url.Parse(sk.Metadata.CPU.Port)
 		if err != nil {
 			return fmt.Errorf("invalid Device URL format: %s", err)
 		}
@@ -118,8 +117,8 @@ func runProgramAction(pm *packagemanager.PackageManager,
 	}
 	logrus.WithField("port", port).Tracef("Upload port")
 
-	if fqbnIn == "" && sketch != nil && sketch.Metadata != nil {
-		fqbnIn = sketch.Metadata.CPU.Fqbn
+	if fqbnIn == "" && sk != nil && sk.Metadata != nil {
+		fqbnIn = sk.Metadata.CPU.Fqbn
 	}
 	if fqbnIn == "" {
 		return fmt.Errorf("no Fully Qualified Board Name provided")
@@ -276,7 +275,7 @@ func runProgramAction(pm *packagemanager.PackageManager,
 	}
 
 	if !burnBootloader {
-		importPath, sketchName, err := determineBuildPathAndSketchName(importFile, importDir, sketch, fqbn)
+		importPath, sketchName, err := determineBuildPathAndSketchName(importFile, importDir, sk, fqbn)
 		if err != nil {
 			return errors.Errorf("retrieving build artifacts: %s", err)
 		}
@@ -431,7 +430,7 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri
 	return nil
 }
 
-func determineBuildPathAndSketchName(importFile, importDir string, sketch *sketches.Sketch, fqbn *cores.FQBN) (*paths.Path, string, error) {
+func determineBuildPathAndSketchName(importFile, importDir string, sk *sketch.Sketch, fqbn *cores.FQBN) (*paths.Path, string, error) {
 	// In general, compiling a sketch will produce a set of files that are
 	// named as the sketch but have different extensions, for example Sketch.ino
 	// may produce: Sketch.ino.bin; Sketch.ino.hex; Sketch.ino.zip; etc...
@@ -478,13 +477,13 @@ func determineBuildPathAndSketchName(importFile, importDir string, sketch *sketc
 	}
 
 	// Case 3: nothing given...
-	if sketch == nil {
+	if sk == nil {
 		return nil, "", fmt.Errorf("no sketch or build directory/file specified")
 	}
 
 	// Case 4: only sketch specified. In this case we use the generated build path
 	// and the given sketch name.
-	return bldr.GenBuildPath(sketch.FullPath), sketch.Name + sketch.MainFileExtension, nil
+	return sk.BuildPath, sk.Name + sk.MainFile.Ext(), nil
 }
 
 func detectSketchNameFromBuildPath(buildPath *paths.Path) (string, error) {
diff --git a/commands/upload/upload_test.go b/commands/upload/upload_test.go
index 99495a1f568..7463700730b 100644
--- a/commands/upload/upload_test.go
+++ b/commands/upload/upload_test.go
@@ -21,10 +21,9 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/arduino/arduino-cli/arduino/builder"
 	"github.com/arduino/arduino-cli/arduino/cores"
 	"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
-	"github.com/arduino/arduino-cli/arduino/sketches"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	paths "github.com/arduino/go-paths-helper"
 	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
@@ -56,13 +55,13 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
 	type test struct {
 		importFile    string
 		importDir     string
-		sketch        *sketches.Sketch
+		sketch        *sketch.Sketch
 		fqbn          *cores.FQBN
 		resBuildPath  string
 		resSketchName string
 	}
 
-	blonk, err := sketches.NewSketchFromPath(paths.New("testdata/Blonk"))
+	blonk, err := sketch.New(paths.New("testdata/Blonk"))
 	require.NoError(t, err)
 
 	fqbn, err := cores.ParseFQBN("arduino:samd:mkr1000")
@@ -78,7 +77,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
 		// 03: error: used both importPath and importFile
 		{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", nil, nil, "<nil>", ""},
 		// 04: only sketch without FQBN
-		{"", "", blonk, nil, builder.GenBuildPath(blonk.FullPath).String(), "Blonk.ino"},
+		{"", "", blonk, nil, sketch.GenBuildPath(blonk.FullPath).String(), "Blonk.ino"},
 		// 05: use importFile to detect build.path and project_name, sketch is ignored.
 		{"testdata/build_path_2/Blink.ino.hex", "", blonk, nil, "testdata/build_path_2", "Blink.ino"},
 		// 06: use importPath as build.path and Blink as project name, ignore the sketch Blonk
@@ -94,7 +93,7 @@ func TestDetermineBuildPathAndSketchName(t *testing.T) {
 		// 11: error: used both importPath and importFile
 		{"testdata/build_path_2/Blink.ino.hex", "testdata/build_path_2", nil, fqbn, "<nil>", ""},
 		// 12: use sketch to determine project name and sketch+fqbn to determine build path
-		{"", "", blonk, fqbn, builder.GenBuildPath(blonk.FullPath).String(), "Blonk.ino"},
+		{"", "", blonk, fqbn, sketch.GenBuildPath(blonk.FullPath).String(), "Blonk.ino"},
 		// 13: use importFile to detect build.path and project_name, sketch+fqbn is ignored.
 		{"testdata/build_path_2/Blink.ino.hex", "", blonk, fqbn, "testdata/build_path_2", "Blink.ino"},
 		// 14: use importPath as build.path and Blink as project name, ignore the sketch Blonk, ignore fqbn
diff --git a/legacy/builder/builder.go b/legacy/builder/builder.go
index 420c459e49b..1d49a70a210 100644
--- a/legacy/builder/builder.go
+++ b/legacy/builder/builder.go
@@ -21,7 +21,7 @@ import (
 	"strconv"
 	"time"
 
-	bldr "github.com/arduino/arduino-cli/arduino/builder"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/legacy/builder/builder_utils"
 	"github.com/arduino/arduino-cli/legacy/builder/constants"
 	"github.com/arduino/arduino-cli/legacy/builder/phases"
@@ -41,7 +41,7 @@ const DEFAULT_SOFTWARE = "ARDUINO"
 type Builder struct{}
 
 func (s *Builder) Run(ctx *types.Context) error {
-	if err := bldr.EnsureBuildPathExists(ctx.BuildPath.String()); err != nil {
+	if err := ctx.BuildPath.MkdirAll(); err != nil {
 		return err
 	}
 
@@ -134,10 +134,10 @@ type Preprocess struct{}
 
 func (s *Preprocess) Run(ctx *types.Context) error {
 	if ctx.BuildPath == nil {
-		ctx.BuildPath = bldr.GenBuildPath(ctx.SketchLocation)
+		ctx.BuildPath = sketch.GenBuildPath(ctx.SketchLocation)
 	}
 
-	if err := bldr.EnsureBuildPathExists(ctx.BuildPath.String()); err != nil {
+	if err := ctx.BuildPath.MkdirAll(); err != nil {
 		return err
 	}
 
@@ -170,7 +170,7 @@ type ParseHardwareAndDumpBuildProperties struct{}
 
 func (s *ParseHardwareAndDumpBuildProperties) Run(ctx *types.Context) error {
 	if ctx.BuildPath == nil {
-		ctx.BuildPath = bldr.GenBuildPath(ctx.SketchLocation)
+		ctx.BuildPath = sketch.GenBuildPath(ctx.SketchLocation)
 	}
 
 	commands := []types.Command{
diff --git a/legacy/builder/container_add_prototypes.go b/legacy/builder/container_add_prototypes.go
index 1a8c093b351..222661ef07b 100644
--- a/legacy/builder/container_add_prototypes.go
+++ b/legacy/builder/container_add_prototypes.go
@@ -32,7 +32,7 @@ func (s *ContainerAddPrototypes) Run(ctx *types.Context) error {
 	targetFilePath := ctx.PreprocPath.Join(constants.FILE_CTAGS_TARGET_FOR_GCC_MINUS_E)
 
 	// Run preprocessor
-	sourceFile := ctx.SketchBuildPath.Join(ctx.Sketch.MainFile.Name.Base() + ".cpp")
+	sourceFile := ctx.SketchBuildPath.Join(ctx.Sketch.MainFile.Base() + ".cpp")
 	if err := GCCPreprocRunner(ctx, sourceFile, targetFilePath, ctx.IncludeFolders); err != nil {
 		return errors.WithStack(err)
 	}
@@ -53,7 +53,7 @@ func (s *ContainerAddPrototypes) Run(ctx *types.Context) error {
 		}
 	}
 
-	if err := bldr.SketchSaveItemCpp(ctx.Sketch.MainFile.Name.String(), []byte(ctx.Source), ctx.SketchBuildPath.String()); err != nil {
+	if err := bldr.SketchSaveItemCpp(ctx.Sketch.MainFile, []byte(ctx.Source), ctx.SketchBuildPath); err != nil {
 		return errors.WithStack(err)
 	}
 
diff --git a/legacy/builder/container_find_includes.go b/legacy/builder/container_find_includes.go
index 6477a5351ed..98c72bfce4a 100644
--- a/legacy/builder/container_find_includes.go
+++ b/legacy/builder/container_find_includes.go
@@ -120,7 +120,7 @@ func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
 	}
 
 	sketch := ctx.Sketch
-	mergedfile, err := types.MakeSourceFile(ctx, sketch, paths.New(sketch.MainFile.Name.Base()+".cpp"))
+	mergedfile, err := types.MakeSourceFile(ctx, sketch, paths.New(sketch.MainFile.Base()+".cpp"))
 	if err != nil {
 		return errors.WithStack(err)
 	}
diff --git a/legacy/builder/container_merge_copy_sketch_files.go b/legacy/builder/container_merge_copy_sketch_files.go
index d62f603f316..b98eaebb721 100644
--- a/legacy/builder/container_merge_copy_sketch_files.go
+++ b/legacy/builder/container_merge_copy_sketch_files.go
@@ -24,22 +24,18 @@ import (
 type ContainerMergeCopySketchFiles struct{}
 
 func (s *ContainerMergeCopySketchFiles) Run(ctx *types.Context) error {
-	sk := types.SketchFromLegacy(ctx.Sketch)
-	if sk == nil {
-		return errors.New("unable to convert legacy sketch to the new type")
-	}
-	offset, source, err := bldr.SketchMergeSources(sk, ctx.SourceOverride)
+	offset, source, err := bldr.SketchMergeSources(ctx.Sketch, ctx.SourceOverride)
 	if err != nil {
 		return err
 	}
 	ctx.LineOffset = offset
 	ctx.Source = source
 
-	if err := bldr.SketchSaveItemCpp(ctx.Sketch.MainFile.Name.String(), []byte(ctx.Source), ctx.SketchBuildPath.String()); err != nil {
+	if err := bldr.SketchSaveItemCpp(ctx.Sketch.MainFile, []byte(ctx.Source), ctx.SketchBuildPath); err != nil {
 		return errors.WithStack(err)
 	}
 
-	if err := bldr.SketchCopyAdditionalFiles(sk, ctx.SketchBuildPath.String(), ctx.SourceOverride); err != nil {
+	if err := bldr.SketchCopyAdditionalFiles(ctx.Sketch, ctx.SketchBuildPath, ctx.SourceOverride); err != nil {
 		return errors.WithStack(err)
 	}
 
diff --git a/legacy/builder/container_setup.go b/legacy/builder/container_setup.go
index 2ce501fcf4f..038f66d92b4 100644
--- a/legacy/builder/container_setup.go
+++ b/legacy/builder/container_setup.go
@@ -18,11 +18,9 @@ package builder
 import (
 	"fmt"
 
-	bldr "github.com/arduino/arduino-cli/arduino/builder"
 	sk "github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/legacy/builder/builder_utils"
 	"github.com/arduino/arduino-cli/legacy/builder/types"
-	"github.com/arduino/go-paths-helper"
 	"github.com/pkg/errors"
 )
 
@@ -63,9 +61,11 @@ func (s *ContainerSetupHardwareToolsLibsSketchAndProps) Run(ctx *types.Context)
 		}
 
 		// load sketch
-		sketch, err := bldr.SketchLoad(sketchLocation.String(), ctx.BuildPath.String())
-		if e, ok := err.(*sk.InvalidSketchFoldernameError); ctx.IgnoreSketchFolderNameErrors && ok {
+		sketch, err := sk.New(sketchLocation)
+		if e, ok := err.(*sk.InvalidSketchFolderNameError); ctx.IgnoreSketchFolderNameErrors && ok {
 			// ignore error
+			// This is only done by the arduino-builder since the Arduino Java IDE
+			// supports sketches with invalid names
 			sketch = e.Sketch
 		} else if err != nil {
 			return errors.WithStack(err)
@@ -73,8 +73,9 @@ func (s *ContainerSetupHardwareToolsLibsSketchAndProps) Run(ctx *types.Context)
 		if sketch.MainFile == nil {
 			return fmt.Errorf("main file missing from sketch")
 		}
-		ctx.SketchLocation = paths.New(sketch.MainFile.Path)
-		ctx.Sketch = types.SketchToLegacy(sketch)
+		sketch.BuildPath = ctx.BuildPath
+		ctx.SketchLocation = sketch.MainFile
+		ctx.Sketch = sketch
 	}
 	ctx.Progress.CompleteStep()
 	builder_utils.PrintProgressIfProgressEnabledAndMachineLogger(ctx)
diff --git a/legacy/builder/create_cmake_rule.go b/legacy/builder/create_cmake_rule.go
index a976819a790..875f76afdcb 100644
--- a/legacy/builder/create_cmake_rule.go
+++ b/legacy/builder/create_cmake_rule.go
@@ -176,7 +176,7 @@ func (s *ExportProjectCMake) Run(ctx *types.Context) error {
 
 	// Generate the CMakeLists global file
 
-	projectName := strings.TrimSuffix(ctx.Sketch.MainFile.Name.Base(), ctx.Sketch.MainFile.Name.Ext())
+	projectName := ctx.Sketch.Name
 
 	cmakelist := "cmake_minimum_required(VERSION 3.5.0)\n"
 	cmakelist += "INCLUDE(FindPkgConfig)\n"
diff --git a/legacy/builder/ctags_runner.go b/legacy/builder/ctags_runner.go
index b132fc8ad03..553e1466d70 100644
--- a/legacy/builder/ctags_runner.go
+++ b/legacy/builder/ctags_runner.go
@@ -56,7 +56,7 @@ func (s *CTagsRunner) Run(ctx *types.Context) error {
 
 	parser := &ctags.CTagsParser{}
 
-	ctx.CTagsOfPreprocessedSource = parser.Parse(ctx.CTagsOutput, ctx.Sketch.MainFile.Name)
+	ctx.CTagsOfPreprocessedSource = parser.Parse(ctx.CTagsOutput, ctx.Sketch.MainFile)
 	parser.FixCLinkageTagsDeclarations(ctx.CTagsOfPreprocessedSource)
 
 	protos, line := parser.GeneratePrototypes()
diff --git a/legacy/builder/filter_sketch_source.go b/legacy/builder/filter_sketch_source.go
index 716b4b4a496..458da993020 100644
--- a/legacy/builder/filter_sketch_source.go
+++ b/legacy/builder/filter_sketch_source.go
@@ -33,9 +33,9 @@ type FilterSketchSource struct {
 
 func (s *FilterSketchSource) Run(ctx *types.Context) error {
 	fileNames := paths.NewPathList()
-	fileNames.Add(ctx.Sketch.MainFile.Name)
-	for _, file := range ctx.Sketch.OtherSketchFiles {
-		fileNames = append(fileNames, file.Name)
+	fileNames.Add(ctx.Sketch.MainFile)
+	for _, file := range *ctx.Sketch.OtherSketchFiles {
+		fileNames = append(fileNames, file)
 	}
 
 	inSketch := false
diff --git a/legacy/builder/merge_sketch_with_bootloader.go b/legacy/builder/merge_sketch_with_bootloader.go
index 0fb350d9c9b..50b4a8809c1 100644
--- a/legacy/builder/merge_sketch_with_bootloader.go
+++ b/legacy/builder/merge_sketch_with_bootloader.go
@@ -42,7 +42,7 @@ func (s *MergeSketchWithBootloader) Run(ctx *types.Context) error {
 
 	buildPath := ctx.BuildPath
 	sketch := ctx.Sketch
-	sketchFileName := sketch.MainFile.Name.Base()
+	sketchFileName := sketch.MainFile.Base()
 
 	sketchInBuildPath := buildPath.Join(sketchFileName + ".hex")
 	sketchInSubfolder := buildPath.Join(constants.FOLDER_SKETCH, sketchFileName+".hex")
diff --git a/legacy/builder/preprocess_sketch.go b/legacy/builder/preprocess_sketch.go
index 10360f09527..c8dee8a91a4 100644
--- a/legacy/builder/preprocess_sketch.go
+++ b/legacy/builder/preprocess_sketch.go
@@ -43,7 +43,7 @@ var ArduinoPreprocessorProperties = properties.NewFromHashmap(map[string]string{
 type PreprocessSketchArduino struct{}
 
 func (s *PreprocessSketchArduino) Run(ctx *types.Context) error {
-	sourceFile := ctx.SketchBuildPath.Join(ctx.Sketch.MainFile.Name.Base() + ".cpp")
+	sourceFile := ctx.SketchBuildPath.Join(ctx.Sketch.MainFile.Base() + ".cpp")
 	commands := []types.Command{
 		&ArduinoPreprocessorRunner{},
 	}
@@ -66,7 +66,7 @@ func (s *PreprocessSketchArduino) Run(ctx *types.Context) error {
 	if ctx.CodeCompleteAt != "" {
 		err = new(OutputCodeCompletions).Run(ctx)
 	} else {
-		err = bldr.SketchSaveItemCpp(ctx.Sketch.MainFile.Name.String(), []byte(ctx.Source), ctx.SketchBuildPath.String())
+		err = bldr.SketchSaveItemCpp(ctx.Sketch.MainFile, []byte(ctx.Source), ctx.SketchBuildPath)
 	}
 
 	return err
diff --git a/legacy/builder/setup_build_properties.go b/legacy/builder/setup_build_properties.go
index 1579180ceba..e102baea351 100644
--- a/legacy/builder/setup_build_properties.go
+++ b/legacy/builder/setup_build_properties.go
@@ -46,7 +46,7 @@ func (s *SetupBuildProperties) Run(ctx *types.Context) error {
 		buildProperties.SetPath("build.path", ctx.BuildPath)
 	}
 	if ctx.Sketch != nil {
-		buildProperties.Set("build.project_name", ctx.Sketch.MainFile.Name.Base())
+		buildProperties.Set("build.project_name", ctx.Sketch.MainFile.Base())
 	}
 	buildProperties.Set("build.arch", strings.ToUpper(targetPlatform.Platform.Architecture))
 
diff --git a/legacy/builder/sketch_loader.go b/legacy/builder/sketch_loader.go
index 276e5583f3d..42a5ce1a0e4 100644
--- a/legacy/builder/sketch_loader.go
+++ b/legacy/builder/sketch_loader.go
@@ -16,14 +16,8 @@
 package builder
 
 import (
-	"sort"
-	"strings"
-
-	"github.com/arduino/arduino-cli/legacy/builder/constants"
-	"github.com/arduino/arduino-cli/legacy/builder/i18n"
+	sk "github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/legacy/builder/types"
-	"github.com/arduino/arduino-cli/legacy/builder/utils"
-	"github.com/arduino/go-paths-helper"
 	"github.com/pkg/errors"
 )
 
@@ -50,70 +44,12 @@ func (s *SketchLoader) Run(ctx *types.Context) error {
 
 	ctx.SketchLocation = sketchLocation
 
-	allSketchFilePaths, err := collectAllSketchFiles(sketchLocation.Parent())
+	sketch, err := sk.New(sketchLocation)
 	if err != nil {
 		return errors.WithStack(err)
 	}
-
-	logger := ctx.GetLogger()
-
-	if !allSketchFilePaths.Contains(sketchLocation) {
-		return i18n.ErrorfWithLogger(logger, constants.MSG_CANT_FIND_SKETCH_IN_PATH, sketchLocation, sketchLocation.Parent())
-	}
-
-	sketch, err := makeSketch(sketchLocation, allSketchFilePaths, ctx.BuildPath, logger)
-	if err != nil {
-		return errors.WithStack(err)
-	}
-
 	ctx.SketchLocation = sketchLocation
 	ctx.Sketch = sketch
 
 	return nil
 }
-
-func collectAllSketchFiles(from *paths.Path) (paths.PathList, error) {
-	filePaths := []string{}
-	// Source files in the root are compiled, non-recursively. This
-	// is the only place where .ino files can be present.
-	rootExtensions := func(ext string) bool { return MAIN_FILE_VALID_EXTENSIONS[ext] || ADDITIONAL_FILE_VALID_EXTENSIONS[ext] }
-	err := utils.FindFilesInFolder(&filePaths, from.String(), rootExtensions, true /* recurse */)
-	if err != nil {
-		return nil, errors.WithStack(err)
-	}
-
-	return paths.NewPathList(filePaths...), errors.WithStack(err)
-}
-
-func makeSketch(sketchLocation *paths.Path, allSketchFilePaths paths.PathList, buildLocation *paths.Path, logger i18n.Logger) (*types.Sketch, error) {
-	sketchFilesMap := make(map[string]types.SketchFile)
-	for _, sketchFilePath := range allSketchFilePaths {
-		sketchFilesMap[sketchFilePath.String()] = types.SketchFile{Name: sketchFilePath}
-	}
-
-	mainFile := sketchFilesMap[sketchLocation.String()]
-	delete(sketchFilesMap, sketchLocation.String())
-
-	additionalFiles := []types.SketchFile{}
-	otherSketchFiles := []types.SketchFile{}
-	mainFileDir := mainFile.Name.Parent()
-	for _, sketchFile := range sketchFilesMap {
-		ext := strings.ToLower(sketchFile.Name.Ext())
-		if MAIN_FILE_VALID_EXTENSIONS[ext] {
-			if sketchFile.Name.Parent().EqualsTo(mainFileDir) {
-				otherSketchFiles = append(otherSketchFiles, sketchFile)
-			}
-		} else if ADDITIONAL_FILE_VALID_EXTENSIONS[ext] {
-			if buildLocation == nil || !strings.Contains(sketchFile.Name.Parent().String(), buildLocation.String()) {
-				additionalFiles = append(additionalFiles, sketchFile)
-			}
-		} else {
-			return nil, i18n.ErrorfWithLogger(logger, constants.MSG_UNKNOWN_SKETCH_EXT, sketchFile.Name)
-		}
-	}
-
-	sort.Sort(types.SketchFileSortByName(additionalFiles))
-	sort.Sort(types.SketchFileSortByName(otherSketchFiles))
-
-	return &types.Sketch{MainFile: mainFile, OtherSketchFiles: otherSketchFiles, AdditionalFiles: additionalFiles}, nil
-}
diff --git a/legacy/builder/test/Baladuino/Baladuino.preprocessed.txt b/legacy/builder/test/Baladuino/Baladuino.preprocessed.txt
index 238739eb902..8636e91422f 100644
--- a/legacy/builder/test/Baladuino/Baladuino.preprocessed.txt
+++ b/legacy/builder/test/Baladuino/Baladuino.preprocessed.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 /*
  * The code is released under the GNU General Public License.
  * Developed by Kristian Lauszus, TKJ Electronics 2013
@@ -87,11 +87,11 @@ WII Wii(&Btd); // The Wii library can communicate with Wiimotes and the Nunchuck
 // This can also be done using the Android or Processing application
 #endif
 
-#line 88 {{QuoteCppString .sketch.MainFile.Name}}
+#line 88 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 204 {{QuoteCppString .sketch.MainFile.Name}}
+#line 204 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 88 {{QuoteCppString .sketch.MainFile.Name}}
+#line 88 {{QuoteCppString .sketch.MainFile}}
 void setup() {
   /* Initialize UART */
   Serial.begin(115200);
diff --git a/legacy/builder/test/CharWithEscapedDoubleQuote/CharWithEscapedDoubleQuote.preprocessed.txt b/legacy/builder/test/CharWithEscapedDoubleQuote/CharWithEscapedDoubleQuote.preprocessed.txt
index 1f9e69a613b..e6363b45861 100644
--- a/legacy/builder/test/CharWithEscapedDoubleQuote/CharWithEscapedDoubleQuote.preprocessed.txt
+++ b/legacy/builder/test/CharWithEscapedDoubleQuote/CharWithEscapedDoubleQuote.preprocessed.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #include <SoftwareSerial.h> // required to send and receive AT commands from the GPRS Shield
 #include <Wire.h> // required for I2C communication with the RTC
 
@@ -38,49 +38,49 @@ Code Exclusively for GPRS shield:
 //  Default set of instructions for GPRS Shield power control
 //
 
-#line 39 {{QuoteCppString .sketch.MainFile.Name}}
+#line 39 {{QuoteCppString .sketch.MainFile}}
 void setPowerStateTo( int newState );
-#line 64 {{QuoteCppString .sketch.MainFile.Name}}
+#line 64 {{QuoteCppString .sketch.MainFile}}
 int getPowerState();
-#line 75 {{QuoteCppString .sketch.MainFile.Name}}
+#line 75 {{QuoteCppString .sketch.MainFile}}
 void powerUpOrDown();
-#line 90 {{QuoteCppString .sketch.MainFile.Name}}
+#line 90 {{QuoteCppString .sketch.MainFile}}
 void clearBufferArray();
-#line 96 {{QuoteCppString .sketch.MainFile.Name}}
+#line 96 {{QuoteCppString .sketch.MainFile}}
 void makeMissedCall( char num[] );
-#line 111 {{QuoteCppString .sketch.MainFile.Name}}
+#line 111 {{QuoteCppString .sketch.MainFile}}
 void sendTextMessage( char number[], char messg[] );
-#line 129 {{QuoteCppString .sketch.MainFile.Name}}
+#line 129 {{QuoteCppString .sketch.MainFile}}
 void analise(byte incoming[], int length);
-#line 179 {{QuoteCppString .sketch.MainFile.Name}}
+#line 179 {{QuoteCppString .sketch.MainFile}}
 byte decToBcd( byte b );
-#line 184 {{QuoteCppString .sketch.MainFile.Name}}
+#line 184 {{QuoteCppString .sketch.MainFile}}
 boolean getBit( byte addr, int pos );
-#line 190 {{QuoteCppString .sketch.MainFile.Name}}
+#line 190 {{QuoteCppString .sketch.MainFile}}
 void setBit( byte addr, int pos, boolean newBit );
-#line 204 {{QuoteCppString .sketch.MainFile.Name}}
+#line 204 {{QuoteCppString .sketch.MainFile}}
 byte getByte( byte addr );
-#line 213 {{QuoteCppString .sketch.MainFile.Name}}
+#line 213 {{QuoteCppString .sketch.MainFile}}
 boolean getBytes( byte addr, int amount );
-#line 230 {{QuoteCppString .sketch.MainFile.Name}}
+#line 230 {{QuoteCppString .sketch.MainFile}}
 void setByte( byte addr, byte newByte );
-#line 235 {{QuoteCppString .sketch.MainFile.Name}}
+#line 235 {{QuoteCppString .sketch.MainFile}}
 void setBytes( byte addr, byte newBytes[], int amount );
-#line 244 {{QuoteCppString .sketch.MainFile.Name}}
+#line 244 {{QuoteCppString .sketch.MainFile}}
 void getTime();
-#line 260 {{QuoteCppString .sketch.MainFile.Name}}
+#line 260 {{QuoteCppString .sketch.MainFile}}
 void setTime( byte newTime[ 7 ] );
-#line 267 {{QuoteCppString .sketch.MainFile.Name}}
+#line 267 {{QuoteCppString .sketch.MainFile}}
 void getRTCTemperature();
-#line 277 {{QuoteCppString .sketch.MainFile.Name}}
+#line 277 {{QuoteCppString .sketch.MainFile}}
 void gprsListen();
-#line 294 {{QuoteCppString .sketch.MainFile.Name}}
+#line 294 {{QuoteCppString .sketch.MainFile}}
 void printTime();
-#line 317 {{QuoteCppString .sketch.MainFile.Name}}
+#line 317 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 334 {{QuoteCppString .sketch.MainFile.Name}}
+#line 334 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 39 {{QuoteCppString .sketch.MainFile.Name}}
+#line 39 {{QuoteCppString .sketch.MainFile}}
 void setPowerStateTo( int newState )
 {
   if( newState != 1 && newState != 0 ) { // tests for an invalid state. In this case no change is made to powerstate
diff --git a/legacy/builder/test/IncludeBetweenMultilineComment/IncludeBetweenMultilineComment.preprocessed.txt b/legacy/builder/test/IncludeBetweenMultilineComment/IncludeBetweenMultilineComment.preprocessed.txt
index 076ff1b3b28..a4890529793 100644
--- a/legacy/builder/test/IncludeBetweenMultilineComment/IncludeBetweenMultilineComment.preprocessed.txt
+++ b/legacy/builder/test/IncludeBetweenMultilineComment/IncludeBetweenMultilineComment.preprocessed.txt
@@ -1,15 +1,15 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #include <CapacitiveSensor.h>
 /*
 #include <WiFi.h>
 */
 CapacitiveSensor cs_13_8 = CapacitiveSensor(13,8);
-#line 6 {{QuoteCppString .sketch.MainFile.Name}}
+#line 6 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 10 {{QuoteCppString .sketch.MainFile.Name}}
+#line 10 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 6 {{QuoteCppString .sketch.MainFile.Name}}
+#line 6 {{QuoteCppString .sketch.MainFile}}
 void setup()
 {
 	Serial.begin(9600);
diff --git a/legacy/builder/test/LineContinuations/LineContinuations.preprocessed.txt b/legacy/builder/test/LineContinuations/LineContinuations.preprocessed.txt
index 227bf26d32a..6338d8366f3 100644
--- a/legacy/builder/test/LineContinuations/LineContinuations.preprocessed.txt
+++ b/legacy/builder/test/LineContinuations/LineContinuations.preprocessed.txt
@@ -1,16 +1,16 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 const char *foo = "\
 hello \
 world\n";
 
 //" delete this comment line and the IDE parser will crash
 
-#line 7 {{QuoteCppString .sketch.MainFile.Name}}
+#line 7 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 11 {{QuoteCppString .sketch.MainFile.Name}}
+#line 11 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 7 {{QuoteCppString .sketch.MainFile.Name}}
+#line 7 {{QuoteCppString .sketch.MainFile}}
 void setup()
 {
 }
diff --git a/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.preprocessed.txt b/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.preprocessed.txt
index 9d943396d6c..4faa5fe83e3 100644
--- a/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.preprocessed.txt
+++ b/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.preprocessed.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #define DEBUG 1
 #define DISABLED 0
 
@@ -15,17 +15,17 @@ typedef int MyType;
 
 #include "empty_2.h"
 
-#line 16 {{QuoteCppString .sketch.MainFile.Name}}
+#line 16 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 21 {{QuoteCppString .sketch.MainFile.Name}}
+#line 21 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 33 {{QuoteCppString .sketch.MainFile.Name}}
+#line 33 {{QuoteCppString .sketch.MainFile}}
 void debug();
-#line 44 {{QuoteCppString .sketch.MainFile.Name}}
+#line 44 {{QuoteCppString .sketch.MainFile}}
 void disabledIsDefined();
-#line 48 {{QuoteCppString .sketch.MainFile.Name}}
+#line 48 {{QuoteCppString .sketch.MainFile}}
 int useMyType(MyType type);
-#line 16 {{QuoteCppString .sketch.MainFile.Name}}
+#line 16 {{QuoteCppString .sketch.MainFile}}
 void setup() {
   // put your setup code here, to run once:
 
diff --git a/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.resolved.directives.txt b/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.resolved.directives.txt
index bf79b2f49c8..c79f372330b 100644
--- a/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.resolved.directives.txt
+++ b/legacy/builder/test/SketchWithIfDef/SketchWithIfDef.resolved.directives.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #define DEBUG 1
 #define DISABLED 0
 
diff --git a/legacy/builder/test/SketchWithStruct/SketchWithStruct.preprocessed.txt b/legacy/builder/test/SketchWithStruct/SketchWithStruct.preprocessed.txt
index 4f15168af15..4dd1ae448a5 100644
--- a/legacy/builder/test/SketchWithStruct/SketchWithStruct.preprocessed.txt
+++ b/legacy/builder/test/SketchWithStruct/SketchWithStruct.preprocessed.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 /* START CODE */
 
 struct A_NEW_TYPE {
@@ -8,13 +8,13 @@ struct A_NEW_TYPE {
   int c;
 } foo;
 
-#line 9 {{QuoteCppString .sketch.MainFile.Name}}
+#line 9 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 13 {{QuoteCppString .sketch.MainFile.Name}}
+#line 13 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 17 {{QuoteCppString .sketch.MainFile.Name}}
+#line 17 {{QuoteCppString .sketch.MainFile}}
 void dostuff (A_NEW_TYPE * bar);
-#line 9 {{QuoteCppString .sketch.MainFile.Name}}
+#line 9 {{QuoteCppString .sketch.MainFile}}
 void setup() {
 
 }
diff --git a/legacy/builder/test/StringWithComment/StringWithComment.preprocessed.txt b/legacy/builder/test/StringWithComment/StringWithComment.preprocessed.txt
index 2fcb13c5ea4..23677ef8ec8 100644
--- a/legacy/builder/test/StringWithComment/StringWithComment.preprocessed.txt
+++ b/legacy/builder/test/StringWithComment/StringWithComment.preprocessed.txt
@@ -1,10 +1,10 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 10 {{QuoteCppString .sketch.MainFile.Name}}
+#line 10 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 void setup() {
   // put your setup code here, to run once:
   // "comment with a double quote
diff --git a/legacy/builder/test/ctags_runner_test.go b/legacy/builder/test/ctags_runner_test.go
index 99434b4cf4d..d47d53845a7 100644
--- a/legacy/builder/test/ctags_runner_test.go
+++ b/legacy/builder/test/ctags_runner_test.go
@@ -216,50 +216,3 @@ func TestCTagsRunnerSketchWithNamespace(t *testing.T) {
 
 	require.Equal(t, expectedOutput, strings.Replace(ctx.CTagsOutput, "\r\n", "\n", -1))
 }
-
-func TestCTagsRunnerSketchWithTemplates(t *testing.T) {
-	DownloadCoresAndToolsAndLibraries(t)
-
-	sketchLocation := Abs(t, paths.New("sketch_with_templates_and_shift", "sketch_with_templates_and_shift.cpp"))
-
-	ctx := &types.Context{
-		HardwareDirs:         paths.NewPathList(filepath.Join("..", "hardware"), "downloaded_hardware"),
-		BuiltInToolsDirs:     paths.NewPathList("downloaded_tools"),
-		BuiltInLibrariesDirs: paths.NewPathList("downloaded_libraries"),
-		OtherLibrariesDirs:   paths.NewPathList("libraries"),
-		SketchLocation:       sketchLocation,
-		FQBN:                 parseFQBN(t, "arduino:avr:leonardo"),
-		ArduinoAPIVersion:    "10600",
-		Verbose:              true,
-	}
-
-	buildPath := SetupBuildPath(t, ctx)
-	defer buildPath.RemoveAll()
-
-	commands := []types.Command{
-
-		&builder.ContainerSetupHardwareToolsLibsSketchAndProps{},
-
-		&builder.ContainerMergeCopySketchFiles{},
-
-		&builder.ContainerFindIncludes{},
-
-		&builder.PrintUsedLibrariesIfVerbose{},
-		&builder.WarnAboutArchIncompatibleLibraries{},
-		&builder.CTagsTargetFileSaver{Source: &ctx.Source, TargetFileName: "ctags_target.cpp"},
-		&builder.CTagsRunner{},
-	}
-
-	for _, command := range commands {
-		err := command.Run(ctx)
-		NoError(t, err)
-	}
-
-	quotedSketchLocation := strings.Replace(sketchLocation.String(), "\\", "\\\\", -1)
-	expectedOutput := "printGyro\t" + quotedSketchLocation + "\t/^void printGyro()$/;\"\tkind:function\tline:10\tsignature:()\treturntype:void\n" +
-		"bVar\t" + quotedSketchLocation + "\t/^c< 8 > bVar;$/;\"\tkind:variable\tline:15\n" +
-		"aVar\t" + quotedSketchLocation + "\t/^c< 1<<8 > aVar;$/;\"\tkind:variable\tline:16\n" +
-		"func\t" + quotedSketchLocation + "\t/^template<int X> func( c< 1<<X> & aParam) {$/;\"\tkind:function\tline:18\tsignature:( c< 1<<X> & aParam)\treturntype:template\n"
-
-	require.Equal(t, expectedOutput, strings.Replace(ctx.CTagsOutput, "\r\n", "\n", -1))
-}
diff --git a/legacy/builder/test/sketch1/merged_sketch.txt b/legacy/builder/test/sketch1/merged_sketch.txt
index 6f21615ced6..a4446b1c1a7 100644
--- a/legacy/builder/test/sketch1/merged_sketch.txt
+++ b/legacy/builder/test/sketch1/merged_sketch.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 void setup() {
 
 }
diff --git a/legacy/builder/test/sketch_with_config/sketch_with_config.preprocessed.txt b/legacy/builder/test/sketch_with_config/sketch_with_config.preprocessed.txt
index 9604504bc00..e633c616300 100644
--- a/legacy/builder/test/sketch_with_config/sketch_with_config.preprocessed.txt
+++ b/legacy/builder/test/sketch_with_config/sketch_with_config.preprocessed.txt
@@ -1,5 +1,5 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #include "config.h"
 
 #ifdef DEBUG
@@ -12,11 +12,11 @@
 
 #include <Bridge.h>
 
-#line 13 {{QuoteCppString .sketch.MainFile.Name}}
+#line 13 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 17 {{QuoteCppString .sketch.MainFile.Name}}
+#line 17 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 13 {{QuoteCppString .sketch.MainFile.Name}}
+#line 13 {{QuoteCppString .sketch.MainFile}}
 void setup() {
 
 }
diff --git a/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.SAM.txt b/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.SAM.txt
index 76e39303fa2..c3c0d7c06d1 100644
--- a/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.SAM.txt
+++ b/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.SAM.txt
@@ -1,17 +1,17 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #if __SAM3X8E__
-#line 2 {{QuoteCppString .sketch.MainFile.Name}}
+#line 2 {{QuoteCppString .sketch.MainFile}}
 void ifBranch();
-#line 9 {{QuoteCppString .sketch.MainFile.Name}}
+#line 9 {{QuoteCppString .sketch.MainFile}}
 void f1();
-#line 10 {{QuoteCppString .sketch.MainFile.Name}}
+#line 10 {{QuoteCppString .sketch.MainFile}}
 void f2();
-#line 12 {{QuoteCppString .sketch.MainFile.Name}}
+#line 12 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 14 {{QuoteCppString .sketch.MainFile.Name}}
+#line 14 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 2 {{QuoteCppString .sketch.MainFile.Name}}
+#line 2 {{QuoteCppString .sketch.MainFile}}
 void ifBranch() {
 }
 #else
diff --git a/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.txt b/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.txt
index 85f2068e48c..7fcfe26c389 100644
--- a/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.txt
+++ b/legacy/builder/test/sketch_with_ifdef/sketch.preprocessed.txt
@@ -1,20 +1,20 @@
 #include <Arduino.h>
-#line 1 {{QuoteCppString .sketch.MainFile.Name}}
+#line 1 {{QuoteCppString .sketch.MainFile}}
 #if __SAM3X8E__
 void ifBranch() {
 }
 #else
-#line 5 {{QuoteCppString .sketch.MainFile.Name}}
+#line 5 {{QuoteCppString .sketch.MainFile}}
 void elseBranch();
-#line 9 {{QuoteCppString .sketch.MainFile.Name}}
+#line 9 {{QuoteCppString .sketch.MainFile}}
 void f1();
-#line 10 {{QuoteCppString .sketch.MainFile.Name}}
+#line 10 {{QuoteCppString .sketch.MainFile}}
 void f2();
-#line 12 {{QuoteCppString .sketch.MainFile.Name}}
+#line 12 {{QuoteCppString .sketch.MainFile}}
 void setup();
-#line 14 {{QuoteCppString .sketch.MainFile.Name}}
+#line 14 {{QuoteCppString .sketch.MainFile}}
 void loop();
-#line 5 {{QuoteCppString .sketch.MainFile.Name}}
+#line 5 {{QuoteCppString .sketch.MainFile}}
 void elseBranch() {
 }
 #endif
diff --git a/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.cpp b/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.cpp
deleted file mode 100644
index f4f1ece849f..00000000000
--- a/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.cpp
+++ /dev/null
@@ -1,19 +0,0 @@
-template<> class FastPin<0> : public _ARMPIN<0, 10, 1 << 10, 0> {};; 
-
-template<> class FastPin<0> : public _ARMPIN<0, 10, 1 < 10, 0> {};; 
-
-template <class SomeType, template <class> class OtherType> class NestedTemplateClass
-{
-  OtherType<SomeType> f;
-};
-
-void printGyro()
-{
-}
-
-template <int P> class c {};
-c< 8 > bVar;
-c< 1<<8 > aVar;
-
-template<int X> func( c< 1<<X> & aParam) {
-}
diff --git a/legacy/builder/types/context.go b/legacy/builder/types/context.go
index 079733db662..e48a8beab35 100644
--- a/legacy/builder/types/context.go
+++ b/legacy/builder/types/context.go
@@ -25,6 +25,7 @@ import (
 	"github.com/arduino/arduino-cli/arduino/libraries"
 	"github.com/arduino/arduino-cli/arduino/libraries/librariesmanager"
 	"github.com/arduino/arduino-cli/arduino/libraries/librariesresolver"
+	"github.com/arduino/arduino-cli/arduino/sketch"
 	"github.com/arduino/arduino-cli/legacy/builder/i18n"
 	rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	paths "github.com/arduino/go-paths-helper"
@@ -68,7 +69,7 @@ type Context struct {
 	BuiltInLibrariesDirs paths.PathList
 	OtherLibrariesDirs   paths.PathList
 	LibraryDirs          paths.PathList // List of paths pointing to individual library root folders
-	SketchLocation       *paths.Path
+	SketchLocation       *paths.Path    // SketchLocation points to the main Sketch file
 	WatchedLocations     paths.PathList
 	ArduinoAPIVersion    string
 	FQBN                 *cores.FQBN
@@ -109,7 +110,7 @@ type Context struct {
 
 	CollectedSourceFiles *UniqueSourceFileQueue
 
-	Sketch          *Sketch
+	Sketch          *sketch.Sketch
 	Source          string
 	SourceGccMinusE string
 	CodeCompletions string
@@ -210,9 +211,9 @@ func (ctx *Context) ExtractBuildOptions() *properties.Map {
 	opts.SetPath("sketchLocation", ctx.SketchLocation)
 	var additionalFilesRelative []string
 	if ctx.Sketch != nil {
-		for _, sketch := range ctx.Sketch.AdditionalFiles {
+		for _, f := range *ctx.Sketch.AdditionalFiles {
 			absPath := ctx.SketchLocation.Parent()
-			relPath, err := sketch.Name.RelTo(absPath)
+			relPath, err := f.RelTo(absPath)
 			if err != nil {
 				continue // ignore
 			}
diff --git a/legacy/builder/types/types.go b/legacy/builder/types/types.go
index 82a8122b13f..70ee1c34608 100644
--- a/legacy/builder/types/types.go
+++ b/legacy/builder/types/types.go
@@ -50,7 +50,7 @@ func MakeSourceFile(ctx *Context, origin interface{}, path *paths.Path) (SourceF
 // appended here.
 func buildRoot(ctx *Context, origin interface{}) *paths.Path {
 	switch o := origin.(type) {
-	case *Sketch:
+	case *sketch.Sketch:
 		return ctx.SketchBuildPath
 	case *libraries.Library:
 		return ctx.LibrariesBuildPath.Join(o.Name)
@@ -64,7 +64,7 @@ func buildRoot(ctx *Context, origin interface{}) *paths.Path {
 // the full path to that source file.
 func sourceRoot(ctx *Context, origin interface{}) *paths.Path {
 	switch o := origin.(type) {
-	case *Sketch:
+	case *sketch.Sketch:
 		return ctx.SketchBuildPath
 	case *libraries.Library:
 		return o.SourceDir
@@ -103,56 +103,6 @@ func (s SketchFileSortByName) Less(i, j int) bool {
 	return s[i].Name.String() < s[j].Name.String()
 }
 
-type Sketch struct {
-	MainFile         SketchFile
-	OtherSketchFiles []SketchFile
-	AdditionalFiles  []SketchFile
-}
-
-func SketchToLegacy(sketch *sketch.Sketch) *Sketch {
-	s := &Sketch{}
-	s.MainFile = SketchFile{
-		paths.New(sketch.MainFile.Path),
-	}
-
-	for _, item := range sketch.OtherSketchFiles {
-		s.OtherSketchFiles = append(s.OtherSketchFiles, SketchFile{
-			paths.New(item.Path),
-		})
-	}
-
-	for _, item := range sketch.AdditionalFiles {
-		s.AdditionalFiles = append(s.AdditionalFiles, SketchFile{
-			paths.New(item.Path),
-		})
-	}
-
-	return s
-}
-
-func SketchFromLegacy(s *Sketch) *sketch.Sketch {
-	others := []*sketch.Item{}
-	for _, f := range s.OtherSketchFiles {
-		i := sketch.NewItem(f.Name.String())
-		others = append(others, i)
-	}
-
-	additional := []*sketch.Item{}
-	for _, f := range s.AdditionalFiles {
-		i := sketch.NewItem(f.Name.String())
-		additional = append(additional, i)
-	}
-
-	return &sketch.Sketch{
-		MainFile: &sketch.Item{
-			Path: s.MainFile.Name.String(),
-		},
-		LocationPath:     s.MainFile.Name.Parent().String(),
-		OtherSketchFiles: others,
-		AdditionalFiles:  additional,
-	}
-}
-
 type PlatforKeysRewrite struct {
 	Rewrites []PlatforKeyRewrite
 }
diff --git a/test/test_compile.py b/test/test_compile.py
index 0d89ed92256..7414f4ad6c1 100644
--- a/test/test_compile.py
+++ b/test/test_compile.py
@@ -130,10 +130,7 @@ def test_compile_with_sketch_with_symlink_selfloop(run_command, data_dir):
 
     # Build sketch for arduino:avr:uno
     result = run_command("compile -b {fqbn} {sketch_path}".format(fqbn=fqbn, sketch_path=sketch_path))
-    # The assertion is a bit relaxed in this case because win behaves differently from macOs and linux
-    # returning a different error detailed message
-    assert "Error during sketch processing" in result.stderr
-    assert not result.ok
+    assert result.ok
 
     sketch_name = "CompileIntegrationTestSymlinkDirLoop"
     sketch_path = os.path.join(data_dir, sketch_name)
@@ -152,10 +149,7 @@ def test_compile_with_sketch_with_symlink_selfloop(run_command, data_dir):
 
     # Build sketch for arduino:avr:uno
     result = run_command("compile -b {fqbn} {sketch_path}".format(fqbn=fqbn, sketch_path=sketch_path))
-    # The assertion is a bit relaxed also in this case because macOS behaves differently from win and linux:
-    # the cli does not follow recursively the symlink til breaking
-    assert "Error during sketch processing" in result.stderr
-    assert not result.ok
+    assert result.ok
 
 
 def test_compile_blacklisted_sketchname(run_command, data_dir):

From 2f2e2a661e19eb5ca0025e898e456dfc3373fc72 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 12:57:38 +0200
Subject: [PATCH 02/10] Add back removed legacy test

---
 legacy/builder/test/ctags_runner_test.go      | 47 +++++++++++++++++++
 .../sketch_with_templates_and_shift.ino       | 19 ++++++++
 2 files changed, 66 insertions(+)
 create mode 100644 legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.ino

diff --git a/legacy/builder/test/ctags_runner_test.go b/legacy/builder/test/ctags_runner_test.go
index d47d53845a7..0f4e8d92ee1 100644
--- a/legacy/builder/test/ctags_runner_test.go
+++ b/legacy/builder/test/ctags_runner_test.go
@@ -216,3 +216,50 @@ func TestCTagsRunnerSketchWithNamespace(t *testing.T) {
 
 	require.Equal(t, expectedOutput, strings.Replace(ctx.CTagsOutput, "\r\n", "\n", -1))
 }
+
+func TestCTagsRunnerSketchWithTemplates(t *testing.T) {
+	DownloadCoresAndToolsAndLibraries(t)
+
+	sketchLocation := Abs(t, paths.New("sketch_with_templates_and_shift", "sketch_with_templates_and_shift.ino"))
+
+	ctx := &types.Context{
+		HardwareDirs:         paths.NewPathList(filepath.Join("..", "hardware"), "downloaded_hardware"),
+		BuiltInToolsDirs:     paths.NewPathList("downloaded_tools"),
+		BuiltInLibrariesDirs: paths.NewPathList("downloaded_libraries"),
+		OtherLibrariesDirs:   paths.NewPathList("libraries"),
+		SketchLocation:       sketchLocation,
+		FQBN:                 parseFQBN(t, "arduino:avr:leonardo"),
+		ArduinoAPIVersion:    "10600",
+		Verbose:              true,
+	}
+
+	buildPath := SetupBuildPath(t, ctx)
+	defer buildPath.RemoveAll()
+
+	commands := []types.Command{
+
+		&builder.ContainerSetupHardwareToolsLibsSketchAndProps{},
+
+		&builder.ContainerMergeCopySketchFiles{},
+
+		&builder.ContainerFindIncludes{},
+
+		&builder.PrintUsedLibrariesIfVerbose{},
+		&builder.WarnAboutArchIncompatibleLibraries{},
+		&builder.CTagsTargetFileSaver{Source: &ctx.Source, TargetFileName: "ctags_target.cpp"},
+		&builder.CTagsRunner{},
+	}
+
+	for _, command := range commands {
+		err := command.Run(ctx)
+		NoError(t, err)
+	}
+
+	quotedSketchLocation := strings.Replace(sketchLocation.String(), "\\", "\\\\", -1)
+	expectedOutput := "printGyro\t" + quotedSketchLocation + "\t/^void printGyro()$/;\"\tkind:function\tline:10\tsignature:()\treturntype:void\n" +
+		"bVar\t" + quotedSketchLocation + "\t/^c< 8 > bVar;$/;\"\tkind:variable\tline:15\n" +
+		"aVar\t" + quotedSketchLocation + "\t/^c< 1<<8 > aVar;$/;\"\tkind:variable\tline:16\n" +
+		"func\t" + quotedSketchLocation + "\t/^template<int X> func( c< 1<<X> & aParam) {$/;\"\tkind:function\tline:18\tsignature:( c< 1<<X> & aParam)\treturntype:template\n"
+
+	require.Equal(t, expectedOutput, strings.Replace(ctx.CTagsOutput, "\r\n", "\n", -1))
+}
diff --git a/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.ino b/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.ino
new file mode 100644
index 00000000000..c0dac664ef2
--- /dev/null
+++ b/legacy/builder/test/sketch_with_templates_and_shift/sketch_with_templates_and_shift.ino
@@ -0,0 +1,19 @@
+template<> class FastPin<0> : public _ARMPIN<0, 10, 1 << 10, 0> {};;
+
+template<> class FastPin<0> : public _ARMPIN<0, 10, 1 < 10, 0> {};;
+
+template <class SomeType, template <class> class OtherType> class NestedTemplateClass
+{
+  OtherType<SomeType> f;
+};
+
+void printGyro()
+{
+}
+
+template <int P> class c {};
+c< 8 > bVar;
+c< 1<<8 > aVar;
+
+template<int X> func( c< 1<<X> & aParam) {
+}

From db9c761496d4d5180a730e422109163455cf595a Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 12:59:06 +0200
Subject: [PATCH 03/10] Changed Sketch struct to not use pointers

---
 arduino/builder/sketch.go              |  4 ++--
 arduino/builder/sketch_test.go         |  4 ++--
 arduino/sketch/sketch.go               | 18 +++++++++---------
 commands/instances.go                  |  6 +++---
 legacy/builder/filter_sketch_source.go |  2 +-
 legacy/builder/types/context.go        |  2 +-
 6 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/arduino/builder/sketch.go b/arduino/builder/sketch.go
index 7b192fab37a..14ae39d4fd9 100644
--- a/arduino/builder/sketch.go
+++ b/arduino/builder/sketch.go
@@ -88,7 +88,7 @@ func SketchMergeSources(sk *sketch.Sketch, overrides map[string]string) (int, st
 	mergedSource += mainSrc + "\n"
 	lineOffset++
 
-	for _, file := range *sk.OtherSketchFiles {
+	for _, file := range sk.OtherSketchFiles {
 		src, err := getSource(file)
 		if err != nil {
 			return 0, "", err
@@ -107,7 +107,7 @@ func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath *paths.Path, over
 		return errors.Wrap(err, "unable to create a folder to save the sketch files")
 	}
 
-	for _, file := range *sketch.AdditionalFiles {
+	for _, file := range sketch.AdditionalFiles {
 		relpath, err := sketch.FullPath.RelTo(file)
 		if err != nil {
 			return errors.Wrap(err, "unable to compute relative path to the sketch for the item")
diff --git a/arduino/builder/sketch_test.go b/arduino/builder/sketch_test.go
index d7fd40b22c5..8ac9c324b2c 100644
--- a/arduino/builder/sketch_test.go
+++ b/arduino/builder/sketch_test.go
@@ -122,7 +122,7 @@ func TestCopyAdditionalFiles(t *testing.T) {
 	require.Equal(t, s2.AdditionalFiles.Len(), 1)
 
 	// save file info
-	info1, err := paths.New(s2.AdditionalFiles.AsStrings()[0]).Stat()
+	info1, err := s2.AdditionalFiles[0].Stat()
 	require.Nil(t, err)
 
 	// copy again
@@ -130,6 +130,6 @@ func TestCopyAdditionalFiles(t *testing.T) {
 	require.Nil(t, err)
 
 	// verify file hasn't changed
-	info2, err := paths.New(s2.AdditionalFiles.AsStrings()[0]).Stat()
+	info2, err := s2.AdditionalFiles[0].Stat()
 	require.Equal(t, info1.ModTime(), info2.ModTime())
 }
diff --git a/arduino/sketch/sketch.go b/arduino/sketch/sketch.go
index 2991f2ad920..a2fe83d4f1d 100644
--- a/arduino/sketch/sketch.go
+++ b/arduino/sketch/sketch.go
@@ -34,9 +34,9 @@ type Sketch struct {
 	MainFile         *paths.Path
 	FullPath         *paths.Path // FullPath is the path to the Sketch folder
 	BuildPath        *paths.Path
-	OtherSketchFiles *paths.PathList // Sketch files that end in .ino other than main file
-	AdditionalFiles  *paths.PathList
-	RootFolderFiles  *paths.PathList // All files that are in the Sketch root
+	OtherSketchFiles paths.PathList // Sketch files that end in .ino other than main file
+	AdditionalFiles  paths.PathList
+	RootFolderFiles  paths.PathList // All files that are in the Sketch root
 	Metadata         *Metadata
 }
 
@@ -80,9 +80,9 @@ func New(path *paths.Path) (*Sketch, error) {
 		MainFile:         mainFile,
 		FullPath:         path,
 		BuildPath:        GenBuildPath(path),
-		OtherSketchFiles: new(paths.PathList),
-		AdditionalFiles:  new(paths.PathList),
-		RootFolderFiles:  new(paths.PathList),
+		OtherSketchFiles: paths.PathList{},
+		AdditionalFiles:  paths.PathList{},
+		RootFolderFiles:  paths.PathList{},
 	}
 
 	err := sketch.checkSketchCasing()
@@ -139,9 +139,9 @@ func New(path *paths.Path) (*Sketch, error) {
 		}
 	}
 
-	sort.Sort(sketch.AdditionalFiles)
-	sort.Sort(sketch.OtherSketchFiles)
-	sort.Sort(sketch.RootFolderFiles)
+	sort.Sort(&sketch.AdditionalFiles)
+	sort.Sort(&sketch.OtherSketchFiles)
+	sort.Sort(&sketch.RootFolderFiles)
 
 	if err := sketch.importMetadata(); err != nil {
 		return nil, fmt.Errorf("importing sketch metadata: %s", err)
diff --git a/commands/instances.go b/commands/instances.go
index 28a541a2285..41d47432dbb 100644
--- a/commands/instances.go
+++ b/commands/instances.go
@@ -849,17 +849,17 @@ func LoadSketch(ctx context.Context, req *rpc.LoadSketchRequest) (*rpc.LoadSketc
 	}
 
 	otherSketchFiles := make([]string, sketch.OtherSketchFiles.Len())
-	for i, file := range *sketch.OtherSketchFiles {
+	for i, file := range sketch.OtherSketchFiles {
 		otherSketchFiles[i] = file.String()
 	}
 
 	additionalFiles := make([]string, sketch.AdditionalFiles.Len())
-	for i, file := range *sketch.AdditionalFiles {
+	for i, file := range sketch.AdditionalFiles {
 		additionalFiles[i] = file.String()
 	}
 
 	rootFolderFiles := make([]string, sketch.RootFolderFiles.Len())
-	for i, file := range *sketch.RootFolderFiles {
+	for i, file := range sketch.RootFolderFiles {
 		rootFolderFiles[i] = file.String()
 	}
 
diff --git a/legacy/builder/filter_sketch_source.go b/legacy/builder/filter_sketch_source.go
index 458da993020..38649845aea 100644
--- a/legacy/builder/filter_sketch_source.go
+++ b/legacy/builder/filter_sketch_source.go
@@ -34,7 +34,7 @@ type FilterSketchSource struct {
 func (s *FilterSketchSource) Run(ctx *types.Context) error {
 	fileNames := paths.NewPathList()
 	fileNames.Add(ctx.Sketch.MainFile)
-	for _, file := range *ctx.Sketch.OtherSketchFiles {
+	for _, file := range ctx.Sketch.OtherSketchFiles {
 		fileNames = append(fileNames, file)
 	}
 
diff --git a/legacy/builder/types/context.go b/legacy/builder/types/context.go
index e48a8beab35..c475460dd0b 100644
--- a/legacy/builder/types/context.go
+++ b/legacy/builder/types/context.go
@@ -211,7 +211,7 @@ func (ctx *Context) ExtractBuildOptions() *properties.Map {
 	opts.SetPath("sketchLocation", ctx.SketchLocation)
 	var additionalFilesRelative []string
 	if ctx.Sketch != nil {
-		for _, f := range *ctx.Sketch.AdditionalFiles {
+		for _, f := range ctx.Sketch.AdditionalFiles {
 			absPath := ctx.SketchLocation.Parent()
 			relPath, err := f.RelTo(absPath)
 			if err != nil {

From 8de0dec7a82f9688b523b0b98662899e8dd76acc Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 12:59:26 +0200
Subject: [PATCH 04/10] Fix small comment

---
 arduino/sketch/sketch.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/arduino/sketch/sketch.go b/arduino/sketch/sketch.go
index a2fe83d4f1d..be978dba72c 100644
--- a/arduino/sketch/sketch.go
+++ b/arduino/sketch/sketch.go
@@ -114,7 +114,7 @@ func New(path *paths.Path) (*Sketch, error) {
 		ext := p.Ext()
 		if _, found := globals.MainFileValidExtensions[ext]; found {
 			if p.EqualsTo(mainFile) {
-				// The main file must be included in the lists of other files
+				// The main file must not be included in the lists of other files
 				continue
 			}
 			// file is a valid sketch file, see if it's stored at the

From 7d00b8348c17e771965d8b5d8c8c1a24c5998389 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 12:59:55 +0200
Subject: [PATCH 05/10] Fix checkSketchCasing to work with .pde files too

---
 arduino/sketch/sketch.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/arduino/sketch/sketch.go b/arduino/sketch/sketch.go
index be978dba72c..f231402799c 100644
--- a/arduino/sketch/sketch.go
+++ b/arduino/sketch/sketch.go
@@ -226,7 +226,11 @@ func (s *Sketch) checkSketchCasing() error {
 	}
 	files.FilterOutDirs()
 
-	files.FilterPrefix(s.Name)
+	candidateFileNames := []string{}
+	for ext := range globals.MainFileValidExtensions {
+		candidateFileNames = append(candidateFileNames, fmt.Sprintf("%s%s", s.Name, ext))
+	}
+	files.FilterPrefix(candidateFileNames...)
 
 	if files.Len() == 0 {
 		sketchFile := s.FullPath.Join(s.Name + globals.MainFileValidExtension)

From ac517bece098ccccc79cf13fee21df4e6944a9f1 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 17:22:44 +0200
Subject: [PATCH 06/10] Add tests loading Sketch with symlinks

---
 arduino/sketch/sketch_test.go                 | 36 +++++++++++++++++++
 .../SketchWithSymlink/SketchWithSymlink.ino   |  2 ++
 .../SketchWithSymlink/some_folder/helper.h    |  0
 .../SketchWithSymlinkLoop.ino                 |  2 ++
 .../some_folder/helper.h                      |  0
 docsgen/go.mod                                |  1 +
 docsgen/go.sum                                |  2 ++
 go.mod                                        |  2 +-
 go.sum                                        |  2 ++
 test/test_compile.py                          | 10 ++++--
 10 files changed, 54 insertions(+), 3 deletions(-)
 create mode 100644 arduino/sketch/testdata/SketchWithSymlink/SketchWithSymlink.ino
 create mode 100644 arduino/sketch/testdata/SketchWithSymlink/some_folder/helper.h
 create mode 100644 arduino/sketch/testdata/SketchWithSymlinkLoop/SketchWithSymlinkLoop.ino
 create mode 100644 arduino/sketch/testdata/SketchWithSymlinkLoop/some_folder/helper.h

diff --git a/arduino/sketch/sketch_test.go b/arduino/sketch/sketch_test.go
index dc53727010e..cf7c8ba457f 100644
--- a/arduino/sketch/sketch_test.go
+++ b/arduino/sketch/sketch_test.go
@@ -300,3 +300,39 @@ func TestCheckForPdeFiles(t *testing.T) {
 	require.Len(t, files, 1)
 	require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
 }
+
+func TestNewSketchWithSymlink(t *testing.T) {
+	sketchPath, _ := paths.New("testdata", "SketchWithSymlink").Abs()
+	mainFilePath := sketchPath.Join("SketchWithSymlink.ino")
+	helperFilePath := sketchPath.Join("some_folder", "helper.h")
+	helperFileSymlinkPath := sketchPath.Join("src", "helper.h")
+	srcPath := sketchPath.Join("src")
+
+	// Create a symlink in the Sketch folder
+	os.Symlink(sketchPath.Join("some_folder").String(), srcPath.String())
+	defer srcPath.Remove()
+
+	sketch, err := New(sketchPath)
+	require.NoError(t, err)
+	require.NotNil(t, sketch)
+	require.True(t, sketch.MainFile.EquivalentTo(mainFilePath))
+	require.True(t, sketch.FullPath.EquivalentTo(sketchPath))
+	require.Equal(t, sketch.OtherSketchFiles.Len(), 0)
+	require.Equal(t, sketch.AdditionalFiles.Len(), 2)
+	require.True(t, sketch.AdditionalFiles.Contains(helperFilePath))
+	require.True(t, sketch.AdditionalFiles.Contains(helperFileSymlinkPath))
+	require.Equal(t, sketch.RootFolderFiles.Len(), 0)
+}
+
+func TestNewSketchWithSymlinkLoop(t *testing.T) {
+	sketchPath, _ := paths.New("testdata", "SketchWithSymlinkLoop").Abs()
+	someSymlinkPath := sketchPath.Join("some_folder", "some_symlink")
+
+	// Create a recursive Sketch symlink
+	os.Symlink(sketchPath.String(), someSymlinkPath.String())
+	defer someSymlinkPath.Remove()
+
+	sketch, err := New(sketchPath)
+	require.Error(t, err)
+	require.Nil(t, sketch)
+}
diff --git a/arduino/sketch/testdata/SketchWithSymlink/SketchWithSymlink.ino b/arduino/sketch/testdata/SketchWithSymlink/SketchWithSymlink.ino
new file mode 100644
index 00000000000..c34fafcb168
--- /dev/null
+++ b/arduino/sketch/testdata/SketchWithSymlink/SketchWithSymlink.ino
@@ -0,0 +1,2 @@
+void setup() { }
+void loop() { }
diff --git a/arduino/sketch/testdata/SketchWithSymlink/some_folder/helper.h b/arduino/sketch/testdata/SketchWithSymlink/some_folder/helper.h
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/arduino/sketch/testdata/SketchWithSymlinkLoop/SketchWithSymlinkLoop.ino b/arduino/sketch/testdata/SketchWithSymlinkLoop/SketchWithSymlinkLoop.ino
new file mode 100644
index 00000000000..c34fafcb168
--- /dev/null
+++ b/arduino/sketch/testdata/SketchWithSymlinkLoop/SketchWithSymlinkLoop.ino
@@ -0,0 +1,2 @@
+void setup() { }
+void loop() { }
diff --git a/arduino/sketch/testdata/SketchWithSymlinkLoop/some_folder/helper.h b/arduino/sketch/testdata/SketchWithSymlinkLoop/some_folder/helper.h
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docsgen/go.mod b/docsgen/go.mod
index f60719925ed..cf7fc2b74c3 100644
--- a/docsgen/go.mod
+++ b/docsgen/go.mod
@@ -6,5 +6,6 @@ replace github.com/arduino/arduino-cli => ../
 
 require (
 	github.com/arduino/arduino-cli v0.0.0
+	github.com/arduino/go-paths-helper v1.6.1 // indirect
 	github.com/spf13/cobra v1.0.1-0.20200710201246-675ae5f5a98c
 )
diff --git a/docsgen/go.sum b/docsgen/go.sum
index b1169c26e2d..8780728b9ba 100644
--- a/docsgen/go.sum
+++ b/docsgen/go.sum
@@ -16,6 +16,8 @@ github.com/arduino/go-paths-helper v1.0.1/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3
 github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
 github.com/arduino/go-paths-helper v1.6.0 h1:S7/d7DqB9XlnvF9KrgSiGmo2oWKmYW6O/DTjj3Bijx4=
 github.com/arduino/go-paths-helper v1.6.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
+github.com/arduino/go-paths-helper v1.6.1 h1:lha+/BuuBsx0qTZ3gy6IO1kU23lObWdQ/UItkzVWQ+0=
+github.com/arduino/go-paths-helper v1.6.1/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
 github.com/arduino/go-properties-orderedmap v1.3.0 h1:4No/vQopB36e7WUIk6H6TxiSEJPiMrVOCZylYmua39o=
 github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk=
 github.com/arduino/go-properties-orderedmap v1.5.0 h1:istmr13qQN3nneuU3lsqlMvI6jqB3u8QUfVU1tX/t/8=
diff --git a/go.mod b/go.mod
index 7511157650c..78ec3f823c0 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.14
 
 require (
 	github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c
-	github.com/arduino/go-paths-helper v1.6.0
+	github.com/arduino/go-paths-helper v1.6.1
 	github.com/arduino/go-properties-orderedmap v1.5.0
 	github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b
 	github.com/arduino/go-win32-utils v0.0.0-20180330194947-ed041402e83b
diff --git a/go.sum b/go.sum
index a063e734c4a..927dd77befc 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,8 @@ github.com/arduino/go-paths-helper v1.5.0 h1:RVo189hD+GhUS1rQ3gixwK1nSbvVR8MGIGa
 github.com/arduino/go-paths-helper v1.5.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
 github.com/arduino/go-paths-helper v1.6.0 h1:S7/d7DqB9XlnvF9KrgSiGmo2oWKmYW6O/DTjj3Bijx4=
 github.com/arduino/go-paths-helper v1.6.0/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
+github.com/arduino/go-paths-helper v1.6.1 h1:lha+/BuuBsx0qTZ3gy6IO1kU23lObWdQ/UItkzVWQ+0=
+github.com/arduino/go-paths-helper v1.6.1/go.mod h1:V82BWgAAp4IbmlybxQdk9Bpkz8M4Qyx+RAFKaG9NuvU=
 github.com/arduino/go-properties-orderedmap v1.3.0 h1:4No/vQopB36e7WUIk6H6TxiSEJPiMrVOCZylYmua39o=
 github.com/arduino/go-properties-orderedmap v1.3.0/go.mod h1:DKjD2VXY/NZmlingh4lSFMEYCVubfeArCsGPGDwb2yk=
 github.com/arduino/go-properties-orderedmap v1.5.0 h1:istmr13qQN3nneuU3lsqlMvI6jqB3u8QUfVU1tX/t/8=
diff --git a/test/test_compile.py b/test/test_compile.py
index 7414f4ad6c1..8fd8996154c 100644
--- a/test/test_compile.py
+++ b/test/test_compile.py
@@ -130,7 +130,10 @@ def test_compile_with_sketch_with_symlink_selfloop(run_command, data_dir):
 
     # Build sketch for arduino:avr:uno
     result = run_command("compile -b {fqbn} {sketch_path}".format(fqbn=fqbn, sketch_path=sketch_path))
-    assert result.ok
+    # The assertion is a bit relaxed in this case because win behaves differently from macOs and linux
+    # returning a different error detailed message
+    assert "Error during build: opening sketch" in result.stderr
+    assert not result.ok
 
     sketch_name = "CompileIntegrationTestSymlinkDirLoop"
     sketch_path = os.path.join(data_dir, sketch_name)
@@ -149,7 +152,10 @@ def test_compile_with_sketch_with_symlink_selfloop(run_command, data_dir):
 
     # Build sketch for arduino:avr:uno
     result = run_command("compile -b {fqbn} {sketch_path}".format(fqbn=fqbn, sketch_path=sketch_path))
-    assert result.ok
+    # The assertion is a bit relaxed in this case because win behaves differently from macOs and linux
+    # returning a different error detailed message
+    assert "Error during build: opening sketch" in result.stderr
+    assert not result.ok
 
 
 def test_compile_blacklisted_sketchname(run_command, data_dir):

From 07f3ae5b88f69f8fc18d6828204605088cea00c2 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Mon, 12 Jul 2021 18:20:08 +0200
Subject: [PATCH 07/10] Update UPGRADING.md

---
 docs/UPGRADING.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md
index 8d3d6cdbfa3..01d80cec118 100644
--- a/docs/UPGRADING.md
+++ b/docs/UPGRADING.md
@@ -4,6 +4,95 @@ Here you can find a list of migration guides to handle breaking changes between
 
 ## Unreleased
 
+### Change public library interface
+
+#### `github.com/arduino/arduino-cli/arduino/builder` package
+
+`GenBuildPath()` function has been moved to `github.com/arduino/arduino-cli/arduino/sketch` package. The signature is
+unchanged.
+
+`EnsureBuildPathExists` function from has been completely removed, in its place use
+`github.com/arduino/go-paths-helper.MkDirAll()`.
+
+`SketchSaveItemCpp` function signature is changed from `path string, contents []byte, destPath string` to
+`path *paths.Path, contents []byte, destPath *paths.Path`. `paths` is `github.com/arduino/go-paths-helper`.
+
+`SketchLoad` function has been removed, in its place use `New` from `github.com/arduino/arduino-cli/arduino/sketch`
+package.
+
+```diff
+-      SketchLoad("/some/path", "")
++      sketch.New(paths.New("some/path))
+}
+```
+
+If you need to set a custom build path you must instead set it after creating the Sketch.
+
+```diff
+-      SketchLoad("/some/path", "/my/build/path")
++      s, err := sketch.New(paths.New("some/path))
++      s.BuildPath = paths.new("/my/build/path")
+}
+```
+
+`SketchCopyAdditionalFiles` function signature is changed from
+`sketch *sketch.Sketch, destPath string, overrides map[string]string` to
+`sketch *sketch.Sketch, destPath *paths.Path, overrides map[string]string`.
+
+#### `github.com/arduino/arduino-cli/arduino/sketch` package
+
+`Item` struct has been removed, use `go-paths-helper.Path` in its place.
+
+`NewItem` has been removed too, use `go-paths-helper.New` in its place.
+
+`GetSourceBytes` has been removed, in its place use `go-paths-helper.Path.ReadFile`. `GetSourceStr` too has been
+removed, in its place:
+
+```diff
+-      s, err := item.GetSourceStr()
++      data, err := file.ReadFile()
++      s := string(data)
+}
+```
+
+`ItemByPath` type and its member functions have been removed, use `go-paths-helper.PathList` in its place.
+
+`Sketch.LocationPath` has been renamed to `FullPath` and its type changed from `string` to `go-paths-helper.Path`.
+
+`Sketch.MainFile` type has changed from `*Item` to `go-paths-helper.Path`. `Sketch.OtherSketchFiles`,
+`Sketch.AdditionalFiles` and `Sketch.RootFolderFiles` type has changed from `[]*Item` to `go-paths-helper.PathList`.
+
+`New` signature has been changed from `sketchFolderPath, mainFilePath, buildPath string, allFilesPaths []string` to
+`path *go-paths-helper.Path`.
+
+`CheckSketchCasing` function is now private, the check is done internally by `New`.
+
+`InvalidSketchFoldernameError` has been renamed `InvalidSketchFolderNameError`.
+
+#### `github.com/arduino/arduino-cli/arduino/sketches` package
+
+`Sketch` struct has been merged with `sketch.Sketch` struct.
+
+`Metadata` and `BoardMetadata` structs have been moved to `github.com/arduino/arduino-cli/arduino/sketch` package.
+
+`NewSketchFromPath` has been deleted, use `sketch.New` in its place.
+
+`ImportMetadata` is now private called internally by `sketch.New`.
+
+`ExportMetadata` has been moved to `github.com/arduino/arduino-cli/arduino/sketch` package.
+
+`BuildPath` has been removed, use `sketch.Sketch.BuildPath` in its place.
+
+`CheckForPdeFiles` has been moved to `github.com/arduino/arduino-cli/arduino/sketch` package.
+
+#### `github.com/arduino/arduino-cli/legacy/builder/types` package
+
+`Sketch` has been removed, use `sketch.Sketch` in its place.
+
+`SketchToLegacy` and `SketchFromLegacy` have been removed, nothing replaces them.
+
+`Context.Sketch` types has been changed from `Sketch` to `sketch.Sketch`.
+
 ### Change of behaviour of gRPC `Init` function
 
 Previously the `Init` function was used to both create a new `CoreInstance` and initialize it, so that the internal

From 9cac5aabd058a145b7bfe2a8a27adbba69476e84 Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Tue, 13 Jul 2021 09:55:51 +0200
Subject: [PATCH 08/10] Clarify a code comment

---
 commands/instances.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/commands/instances.go b/commands/instances.go
index 41d47432dbb..cc8fd1ed92f 100644
--- a/commands/instances.go
+++ b/commands/instances.go
@@ -842,7 +842,7 @@ func Upgrade(ctx context.Context, req *rpc.UpgradeRequest, downloadCB DownloadPr
 
 // LoadSketch collects and returns all files composing a sketch
 func LoadSketch(ctx context.Context, req *rpc.LoadSketchRequest) (*rpc.LoadSketchResponse, error) {
-	// TODO: This a ToRpc function for the Sketch struct
+	// TODO: This should be a ToRpc function for the Sketch struct
 	sketch, err := sk.New(paths.New(req.SketchPath))
 	if err != nil {
 		return nil, fmt.Errorf("error loading sketch %v: %v", req.SketchPath, err)

From bf30333a078d63cef0033b4931253458e01196cc Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Tue, 13 Jul 2021 10:04:43 +0200
Subject: [PATCH 09/10] Add some more tests for Sketches with symlinks

---
 arduino/sketch/sketch_test.go                 | 55 ++++++++++++++++++-
 .../SketchWithMultipleSymlinkLoops.ino        |  2 +
 .../src/UpGoer1                               |  1 +
 .../src/UpGoer2                               |  1 +
 4 files changed, 58 insertions(+), 1 deletion(-)
 create mode 100644 arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/SketchWithMultipleSymlinkLoops.ino
 create mode 120000 arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
 create mode 120000 arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2

diff --git a/arduino/sketch/sketch_test.go b/arduino/sketch/sketch_test.go
index cf7c8ba457f..9c336d3900a 100644
--- a/arduino/sketch/sketch_test.go
+++ b/arduino/sketch/sketch_test.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/arduino/go-paths-helper"
 	"github.com/stretchr/testify/assert"
@@ -332,7 +333,59 @@ func TestNewSketchWithSymlinkLoop(t *testing.T) {
 	os.Symlink(sketchPath.String(), someSymlinkPath.String())
 	defer someSymlinkPath.Remove()
 
-	sketch, err := New(sketchPath)
+	// The failure condition is New() never returning, testing for which requires setting up a timeout.
+	done := make(chan bool)
+	var sketch *Sketch
+	var err error
+	go func() {
+		sketch, err = New(sketchPath)
+		done <- true
+	}()
+
+	assert.Eventually(
+		t,
+		func() bool {
+			select {
+			case <-done:
+				return true
+			default:
+				return false
+			}
+		},
+		20*time.Second,
+		10*time.Millisecond,
+		"Infinite symlink loop while loading sketch",
+	)
+	require.Error(t, err)
+	require.Nil(t, sketch)
+}
+
+func TestSketchWithMultipleSymlinkLoops(t *testing.T) {
+	sketchPath, _ := paths.New("testdata", "SketchWithMultipleSymlinkLoops").Abs()
+
+	// The failure condition is New() never returning, testing for which requires setting up a timeout.
+	done := make(chan bool)
+	var sketch *Sketch
+	var err error
+	go func() {
+		sketch, err = New(sketchPath)
+		done <- true
+	}()
+
+	assert.Eventually(
+		t,
+		func() bool {
+			select {
+			case <-done:
+				return true
+			default:
+				return false
+			}
+		},
+		20*time.Second,
+		10*time.Millisecond,
+		"Infinite symlink loop while loading sketch",
+	)
 	require.Error(t, err)
 	require.Nil(t, sketch)
 }
diff --git a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/SketchWithMultipleSymlinkLoops.ino b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/SketchWithMultipleSymlinkLoops.ino
new file mode 100644
index 00000000000..cb344de719a
--- /dev/null
+++ b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/SketchWithMultipleSymlinkLoops.ino
@@ -0,0 +1,2 @@
+void setup(){}
+void loop(){}
diff --git a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1 b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
new file mode 120000
index 00000000000..a96aa0ea9d8
--- /dev/null
+++ b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
@@ -0,0 +1 @@
+..
\ No newline at end of file
diff --git a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2 b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2
new file mode 120000
index 00000000000..a96aa0ea9d8
--- /dev/null
+++ b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2
@@ -0,0 +1 @@
+..
\ No newline at end of file

From 9618e9d8eda538166b75b929d2d0549fa05b1e4f Mon Sep 17 00:00:00 2001
From: Silvano Cerza <silvanocerza@gmail.com>
Date: Tue, 13 Jul 2021 11:07:19 +0200
Subject: [PATCH 10/10] Fix Sketch symlinks tests for Windows

---
 arduino/sketch/sketch_test.go                   | 17 +++++++++++++----
 .../SketchWithMultipleSymlinkLoops/src/UpGoer1  |  1 -
 .../SketchWithMultipleSymlinkLoops/src/UpGoer2  |  1 -
 3 files changed, 13 insertions(+), 6 deletions(-)
 delete mode 120000 arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
 delete mode 120000 arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2

diff --git a/arduino/sketch/sketch_test.go b/arduino/sketch/sketch_test.go
index 9c336d3900a..f203491b250 100644
--- a/arduino/sketch/sketch_test.go
+++ b/arduino/sketch/sketch_test.go
@@ -330,13 +330,13 @@ func TestNewSketchWithSymlinkLoop(t *testing.T) {
 	someSymlinkPath := sketchPath.Join("some_folder", "some_symlink")
 
 	// Create a recursive Sketch symlink
-	os.Symlink(sketchPath.String(), someSymlinkPath.String())
+	err := os.Symlink(sketchPath.String(), someSymlinkPath.String())
+	require.NoErrorf(t, err, "This test must be run as administrator on Windows to have symlink creation privilege.")
 	defer someSymlinkPath.Remove()
 
 	// The failure condition is New() never returning, testing for which requires setting up a timeout.
 	done := make(chan bool)
 	var sketch *Sketch
-	var err error
 	go func() {
 		sketch, err = New(sketchPath)
 		done <- true
@@ -361,12 +361,21 @@ func TestNewSketchWithSymlinkLoop(t *testing.T) {
 }
 
 func TestSketchWithMultipleSymlinkLoops(t *testing.T) {
-	sketchPath, _ := paths.New("testdata", "SketchWithMultipleSymlinkLoops").Abs()
+	sketchPath := paths.New("testdata", "SketchWithMultipleSymlinkLoops")
+	srcPath := sketchPath.Join("src")
+	srcPath.Mkdir()
+	defer srcPath.RemoveAll()
+
+	firstSymlinkPath := srcPath.Join("UpGoer1")
+	secondSymlinkPath := srcPath.Join("UpGoer2")
+	err := os.Symlink("..", firstSymlinkPath.String())
+	require.NoErrorf(t, err, "This test must be run as administrator on Windows to have symlink creation privilege.")
+	err = os.Symlink("..", secondSymlinkPath.String())
+	require.NoErrorf(t, err, "This test must be run as administrator on Windows to have symlink creation privilege.")
 
 	// The failure condition is New() never returning, testing for which requires setting up a timeout.
 	done := make(chan bool)
 	var sketch *Sketch
-	var err error
 	go func() {
 		sketch, err = New(sketchPath)
 		done <- true
diff --git a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1 b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
deleted file mode 120000
index a96aa0ea9d8..00000000000
--- a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer1
+++ /dev/null
@@ -1 +0,0 @@
-..
\ No newline at end of file
diff --git a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2 b/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2
deleted file mode 120000
index a96aa0ea9d8..00000000000
--- a/arduino/sketch/testdata/SketchWithMultipleSymlinkLoops/src/UpGoer2
+++ /dev/null
@@ -1 +0,0 @@
-..
\ No newline at end of file