diff --git a/cmd/arduino-flasher-cli/flash/flash.go b/cmd/arduino-flasher-cli/flash/flash.go index e12b58c..d7f5862 100644 --- a/cmd/arduino-flasher-cli/flash/flash.go +++ b/cmd/arduino-flasher-cli/flash/flash.go @@ -32,7 +32,7 @@ import ( ) func NewFlashCmd() *cobra.Command { - var forceYes bool + var forceYes, preserveUser bool var tempDir string appCmd := &cobra.Command{ Use: "flash", @@ -70,12 +70,12 @@ NOTE: On Windows, required drivers are automatically installed with elevated pri Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { checkDriversInstalled() - runFlashCommand(cmd.Context(), args, forceYes, tempDir) + runFlashCommand(cmd.Context(), args, forceYes, preserveUser, tempDir) }, } appCmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "Automatically confirm all prompts") appCmd.Flags().StringVar(&tempDir, "temp-dir", "", "Path to the directory in which the image will be downloaded and extracted") - // TODO: add --clean-install flag or something similar to distinguish between keeping and purging the /home directory + appCmd.Flags().BoolVar(&preserveUser, "preserve-user", false, "Preserve user partition") return appCmd } @@ -91,13 +91,13 @@ func checkDriversInstalled() { } } -func runFlashCommand(ctx context.Context, args []string, forceYes bool, tempDir string) { +func runFlashCommand(ctx context.Context, args []string, forceYes bool, preserveUser bool, tempDir string) { imagePath, err := paths.New(args[0]).Abs() if err != nil { feedback.Fatal(i18n.Tr("could not find image absolute path: %v", err), feedback.ErrBadArgument) } - if !forceYes { + if !forceYes && !preserveUser { feedback.Print("\nWARNING: flashing a new Linux image on the board will erase any existing data you have on it.") feedback.Printf("Do you want to proceed and flash %s on the board? (yes/no)", args[0]) @@ -113,7 +113,7 @@ func runFlashCommand(ctx context.Context, args []string, forceYes bool, tempDir } } - err = updater.Flash(ctx, imagePath, args[0], forceYes, tempDir) + err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir) if err != nil { feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument) } diff --git a/internal/updater/artifacts/artifacts_read_xml.go b/internal/updater/artifacts/artifacts_read_xml.go new file mode 100644 index 0000000..008a1e2 --- /dev/null +++ b/internal/updater/artifacts/artifacts_read_xml.go @@ -0,0 +1,23 @@ +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 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-flasher-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 artifacts + +import ( + _ "embed" +) + +//go:embed read.xml +var ReadXML []byte diff --git a/internal/updater/artifacts/read.xml b/internal/updater/artifacts/read.xml new file mode 100644 index 0000000..2696c87 --- /dev/null +++ b/internal/updater/artifacts/read.xml @@ -0,0 +1,4 @@ + + + + diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index 182dbf5..5f3c60f 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -17,8 +17,11 @@ package updater import ( "context" + "encoding/hex" "fmt" "runtime" + "strconv" + "strings" "github.com/arduino/go-paths-helper" "github.com/shirou/gopsutil/v4/disk" @@ -31,8 +34,9 @@ import ( const GiB = uint64(1024 * 1024 * 1024) const DownloadDiskSpace = uint64(12) const ExtractDiskSpace = uint64(10) +const yesPrompt = "yes" -func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, tempDir string) error { +func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string) error { if !imagePath.Exist() { temp, err := SetTempDir("download-", tempDir) if err != nil { @@ -86,10 +90,10 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes imagePath = tempContent[0] } - return FlashBoard(ctx, imagePath.String(), version) + return FlashBoard(ctx, imagePath.String(), version, preserveUser) } -func FlashBoard(ctx context.Context, downloadedImagePath string, version string) error { +func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool) error { var flashDir *paths.Path for _, entry := range []string{"flash", "flash_UnoQ"} { if p := paths.New(downloadedImagePath, entry); p.Exist() { @@ -125,9 +129,40 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string) if err != nil { return err } - // TODO: add logic to preserve the user partition + + rawProgram := "rawprogram0.xml" + if preserveUser { + if errT := checkBoardGPTTable(ctx, qdlPath, flashDir); errT == nil && flashDir.Join("rawprogram0.nouser.xml").Exist() { + rawProgram = "rawprogram0.nouser.xml" + } else { + res, err := func(target string) (bool, error) { + warnStr := "Linux image " + target + " does not support user partition preservation" + if errT != nil { + warnStr = errT.Error() + } + feedback.Printf("\nWARNING: %s.", warnStr) + feedback.Printf("Do you want to proceed and flash %s on the board, erasing any existing data you have on it? (yes/no)", target) + + var yesInput string + _, err := fmt.Scanf("%s\n", &yesInput) + if err != nil { + return false, err + } + yes := strings.ToLower(yesInput) == yesPrompt || strings.ToLower(yesInput) == "y" + return yes, nil + }(version) + if err != nil { + return err + } + if !res { + return fmt.Errorf("flashing not confirmed by user, exiting") + } + } + + } + feedback.Print(i18n.Tr("Flashing with qdl")) - cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", "rawprogram0.xml", "patch0.xml") + cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", rawProgram, "patch0.xml") if err != nil { return err } @@ -141,3 +176,52 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string) return nil } + +func checkBoardGPTTable(ctx context.Context, qdlPath, flashDir *paths.Path) error { + dumpBinPath := qdlPath.Parent().Join("dump.bin") + readXMLPath := qdlPath.Parent().Join("read.xml") + err := readXMLPath.WriteFile(artifacts.ReadXML) + if err != nil { + return err + } + cmd, err := paths.NewProcess(nil, qdlPath.String(), "--storage", "emmc", flashDir.Join("prog_firehose_ddr.elf").String(), readXMLPath.String()) + if err != nil { + return err + } + cmd.SetDir(qdlPath.Parent().String()) + if err := cmd.RunWithinContext(ctx); err != nil { + return err + } + if !dumpBinPath.Exist() { + return fmt.Errorf("it was not possible to access the current Debian image GPT table") + } + dump, err := dumpBinPath.ReadFile() + if err != nil { + return err + } + strDump := hex.Dump(dump) + + strDumpSlice := strings.Split(strDump, "\n") + // the max number of partitions is stored at entry 0x50 + maxPartitions, err := strconv.ParseInt(strings.Split(strDumpSlice[5], " ")[2], 16, 16) + if err != nil { + return err + } + + numPartitions := 0 + // starting from entry 0x200, there is a new partition every 0x80 bytes + // TODO: check if the size of each partition is 80h or just assume it? + for i := 32; numPartitions < int(maxPartitions); i += 8 { + // partitions are made of non-zero bytes, if all 0s then there are no more entries + if strings.Contains(strDumpSlice[i], "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00") { + break + } + numPartitions++ + } + + if numPartitions == 73 && maxPartitions == 76 { + return fmt.Errorf("the current Debian image (R0) does not support user partition preservation") + } + + return nil +}