diff --git a/Encelado/.gitignore b/Encelado/.gitignore index f29207e..7bea176 100644 --- a/Encelado/.gitignore +++ b/Encelado/.gitignore @@ -11,3 +11,8 @@ logs/ # Local configuration: credentials and machine-specific overrides live here *.local.json .env + +# Il token di Gitea per la catena di rilascio. Il modello versionato è +# build/gitea.example.json; questo file contiene una credenziale e non entra +# mai nel repository. +build/gitea.json diff --git a/Encelado/.vscode/tasks.json b/Encelado/.vscode/tasks.json index 7d80c7d..a94bfaa 100644 --- a/Encelado/.vscode/tasks.json +++ b/Encelado/.vscode/tasks.json @@ -1,83 +1,142 @@ { + // Tutte le attività passano da build/Release.proj: la catena è un solo file + // MSBuild versionato col codice, e qui restano soltanto i nomi e le domande. + // MSBuild non può chiedere niente a nessuno — i prompt stanno in "inputs". + // + // È la stessa impostazione di Mimante/AutoBidder. "version": "2.0.0", "tasks": [ { - // Referenced by the launch configuration as preLaunchTask. "label": "build", + "detail": "Compilazione di debug, per F5 e per il controllo rapido degli errori.", "type": "process", "command": "dotnet", "args": [ "build", "${workspaceFolder}/Encelado.slnx", - "-p:GenerateFullPaths=true", - "-consoleloggerparameters:NoSummary" + "-c", + "Debug", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" ], "group": { "kind": "build", "isDefault": true }, - "problemMatcher": "$msCompile", - "presentation": { "reveal": "silent", "clear": true } + "problemMatcher": "$msCompile" }, { - "label": "test", + "label": "verifica", + "detail": "Compila e lancia i test. Da eseguire dopo ogni modifica.", "type": "process", "command": "dotnet", - "args": ["test", "${workspaceFolder}/Encelado.slnx", "--nologo"], + "args": [ + "msbuild", + "${workspaceFolder}/build/Release.proj", + "-t:Verifica", + "-nologo", + "-v:m" + ], "group": { "kind": "test", "isDefault": true }, "problemMatcher": "$msCompile" }, { - // Produces the distributable build under src/Encelado.Bot/publish. - "label": "publish (Release)", + "label": "backtest", + "detail": "Rigioca una serie storica di prezzi: taratura contro verifica, con il confronto sul comprare e tenere.", "type": "process", "command": "dotnet", "args": [ - "publish", - "${workspaceFolder}/src/Encelado.Bot/Encelado.Bot.csproj", - "-c", - "Release", - "-o", - "${workspaceFolder}/src/Encelado.Bot/publish" + "msbuild", + "${workspaceFolder}/build/Release.proj", + "-t:Backtest", + "-p:Dati=${input:dati}", + "-p:Comando=${input:comando}", + "-nologo", + "-v:m" ], + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": [] + }, + { + "label": "crea installatore", + "detail": "Verifica, pubblica ed esegue Inno Setup: bin/installer/Encelado--setup.exe. Crea il tag a pacchetto pronto. Non tocca Gitea.", + "type": "process", + "command": "dotnet", + "args": [ + "msbuild", + "${workspaceFolder}/build/Release.proj", + "-t:Pacchetto", + "-p:Versione=${input:versione}", + "-nologo", + "-v:m" + ], + "presentation": { "reveal": "always", "panel": "dedicated" }, "problemMatcher": "$msCompile" }, { - // Produce artifacts/installer/Encelado-Setup-.exe: un unico file da - // consegnare. Installa Inno Setup al primo utilizzo se non lo trova. - "label": "installer", - "type": "process", - "command": "powershell.exe", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "${workspaceFolder}/build/make-installer.ps1" - ], - "problemMatcher": [], - "presentation": { "reveal": "always", "panel": "dedicated", "clear": true } - }, - { - // Come sopra ma senza il runtime .NET incorporato: ~2 MB invece di ~65, al - // prezzo di dover avere il .NET 10 Desktop Runtime sulla macchina di arrivo. - "label": "installer (senza runtime)", - "type": "process", - "command": "powershell.exe", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "${workspaceFolder}/build/make-installer.ps1", - "-FrameworkDependent" - ], - "problemMatcher": [], - "presentation": { "reveal": "always", "panel": "dedicated", "clear": true } - }, - { - "label": "clean", + "label": "crea installatore (senza rieseguire i test)", + "detail": "Solo pubblicazione e Inno Setup. Da usare quando i test sono appena passati.", "type": "process", "command": "dotnet", - "args": ["clean", "${workspaceFolder}/Encelado.slnx"], - "problemMatcher": [] + "args": [ + "msbuild", + "${workspaceFolder}/build/Release.proj", + "-t:Pacchetto", + "-p:Versione=${input:versione}", + "-p:SaltaVerifica=true", + "-nologo", + "-v:m" + ], + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": "$msCompile" + }, + { + "label": "rilascia su Gitea", + "detail": "Verifica, pubblica, installatore, tag e release su Gitea con i file allegati. La versione viene dal tag su HEAD. Richiede build/gitea.json.", + "type": "process", + "command": "dotnet", + "args": [ + "msbuild", + "${workspaceFolder}/build/Release.proj", + "-t:Rilascia", + "-p:Versione=${input:versione}", + "-nologo", + "-v:m" + ], + // Le note passano dall'ambiente, non da -p:. MSBuild spezza il valore di + // una proprietà sulle virgole e una nota in italiano ne ha quasi sempre + // una: si otterrebbe MSB1006 «proprietà non valida». Vedi Release.proj. + "options": { + "env": { + "ENCELADO_NOTE": "${input:note}" + } + }, + "presentation": { "reveal": "always", "panel": "dedicated" }, + "problemMatcher": "$msCompile" + } + ], + "inputs": [ + { + "id": "versione", + "type": "promptString", + "description": "Versione — lascia vuoto se hai già taggato (git tag v3.3.0), o per la minor successiva", + "default": "" + }, + { + "id": "note", + "type": "promptString", + "description": "Note di rilascio (vuoto = solo il numero di versione)", + "default": "" + }, + { + "id": "dati", + "type": "promptString", + "description": "File CSV con la serie storica dei prezzi", + "default": "C:\\Users\\alber\\Downloads\\BTC\\btcusd_bitstamp_1min_2012-2025.csv" + }, + { + "id": "comando", + "type": "pickString", + "description": "Cosa misurare", + "options": ["split", "walk", "frequency", "costs", "sweep", "explore"], + "default": "split" } ] } diff --git a/Encelado/Modifiche.txt b/Encelado/Modifiche.txt new file mode 100644 index 0000000..ff3a58d --- /dev/null +++ b/Encelado/Modifiche.txt @@ -0,0 +1,4 @@ +Cose da fare: + - Non caricare i sorgenti del programma nella release. Deve solo essere presente la versione portable e la versione installabile. + - Il nome del setup deve essere Encelado_Versione.exe (ad esempio Encelado_4.14.0.exe) + \ No newline at end of file diff --git a/Encelado/build/Encelado.iss b/Encelado/build/Encelado.iss new file mode 100644 index 0000000..f0c32b0 --- /dev/null +++ b/Encelado/build/Encelado.iss @@ -0,0 +1,153 @@ +; ───────────────────────────────────────────────────────────────────────────── +; Encelado — script di installazione (Inno Setup 6) +; +; Non si compila a mano: lo lancia build/Release.proj, che prima pubblica +; l'applicazione e poi passa qui versione e percorsi con /D. Compilarlo da solo +; produrrebbe un pacchetto con la versione sbagliata, perché il numero vive nel +; tag git e non in questo file. +; +; dotnet msbuild build/Release.proj -t:Pacchetto +; +; ── Due differenze rispetto ad AutoBidder.iss ─────────────────────────────── +; +; La prima: Encelado non è un eseguibile unico. È una cartella — l'applicazione +; legge encelado.json accanto a sé — quindi si copia SourceDir, non SourceExe. +; +; La seconda: l'installazione è per utente e non è possibile forzarla altrove. +; Non è per evitare l'UAC. Encelado scrive log, diario operazioni e CSV di +; analisi accanto al proprio eseguibile: dentro C:\Program Files quelle +; scritture fallirebbero, e siccome il logger degrada in silenzio piuttosto che +; fermare il bot, l'utente se ne accorgerebbe solo cercando i log per capire +; cosa è successo — cioè nel momento peggiore. +; ───────────────────────────────────────────────────────────────────────────── + +#ifndef AppVersion + #define AppVersion "0.0.0" +#endif + +#ifndef SourceDir + #define SourceDir "..\bin\publish\win-x64" +#endif + +#ifndef OutputDir + #define OutputDir "..\bin\installer" +#endif + +#define AppName "Encelado" +#define AppPublisher "Alberto Balbo" +#define AppExeName "Encelado.exe" +#define AppDescription "Bot di trading automatico su Alpaca" + +[Setup] +; L'AppId identifica il prodotto fra una versione e l'altra: cambiarlo farebbe +; comparire due voci in "App installate" invece di un aggiornamento. +AppId={{7C4F1E62-2B8A-4D19-9C55-3E0A6B1D8F44} +AppName={#AppName} +AppVersion={#AppVersion} +AppVerName={#AppName} {#AppVersion} +AppPublisher={#AppPublisher} +VersionInfoVersion={#AppVersion} +VersionInfoDescription={#AppDescription} + +; Vedi la nota in testa al file: l'applicazione deve poter scrivere nella +; propria cartella, quindi l'installazione resta nel profilo dell'utente e non +; è consentito spostarla altrove. +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed= +DefaultDirName={autopf}\{#AppName} +DefaultGroupName={#AppName} +DisableProgramGroupPage=yes +DisableDirPage=auto + +OutputDir={#OutputDir} +OutputBaseFilename=Encelado_{#AppVersion} +SetupIconFile=..\src\Encelado.Bot\Assets\encelado.ico +UninstallDisplayIcon={app}\{#AppExeName} +UninstallDisplayName={#AppName} {#AppVersion} + +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible + +; Se Encelado è in esecuzione, il Restart Manager lo chiude invece di lasciare +; l'installazione a metà con i file bloccati. +CloseApplications=yes +RestartApplications=no + +[Languages] +Name: "italiano"; MessagesFile: "compiler:Languages\Italian.isl" + +[Tasks] +Name: "desktopicon"; Description: "Crea un collegamento sul desktop"; GroupDescription: "Collegamenti:" + +[Files] +; Tutto il publish tranne la configurazione, che ha una regola sua, e i simboli +; di debug, che non servono a chi installa. +Source: "{#SourceDir}\*"; DestDir: "{app}"; \ + Excludes: "encelado.json,*.pdb,*.xml,logs\*"; \ + Flags: ignoreversion recursesubdirs createallsubdirs + +; La configurazione è il prodotto — ogni numero dentro encelado.json è tarato su +; due dataset indipendenti — ma è anche l'unico posto dove l'utente mette mano, +; dalla scheda Impostazioni o a mano. "onlyifdoesntexist" fa sì che un +; aggiornamento non cancelli quelle modifiche; "uninsneveruninstall" che una +; disinstallazione non le butti via. Le chiavi nuove introdotte da una versione +; successiva non rompono nulla: il loader usa i valori di default per quelle che +; non trova. +Source: "{#SourceDir}\encelado.json"; DestDir: "{app}"; \ + Flags: onlyifdoesntexist uninsneveruninstall + +; Copia sempre aggiornata dei valori di fabbrica, per poter vedere cosa è +; cambiato rispetto al proprio encelado.json dopo un aggiornamento. +Source: "{#SourceDir}\encelado.json"; DestDir: "{app}"; \ + DestName: "encelado.default.json"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Comment: "{#AppDescription}" +Name: "{group}\Disinstalla {#AppName}"; Filename: "{uninstallexe}" +Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; \ + Comment: "{#AppDescription}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#AppExeName}"; Description: "Avvia {#AppName}"; \ + Flags: nowait postinstall skipifsilent + +[UninstallDelete] +; Prodotti a runtime, quindi non tracciati dall'installatore: senza questo +; resterebbero una cartella e dei file orfani. +Type: filesandordirs; Name: "{app}\logs" +Type: dirifempty; Name: "{app}" + +[Code] +{ Le credenziali Alpaca vivono in %LocalAppData%\Encelado, fuori dalla cartella + di installazione, quindi una disinstallazione normale non le toccherebbe. + Lasciarle lì in silenzio però significa lasciare sul disco una chiave API + cifrata di cui l'utente si è dimenticato. Glielo chiediamo, con il "no" come + risposta predefinita: chi disinstalla per reinstallare una versione nuova non + deve ritrovarsi a reinserire le chiavi solo perché ha premuto Invio di fretta. } +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + DataDir: String; +begin + if CurUninstallStep <> usPostUninstall then + Exit; + + { In modalità silenziosa non c'è nessuno a cui chiedere, e la risposta che non + si può disfare è quella che cancella. Nel dubbio le credenziali restano. } + if UninstallSilent then + Exit; + + DataDir := ExpandConstant('{localappdata}\Encelado'); + if not DirExists(DataDir) then + Exit; + + if MsgBox( + 'Vuoi eliminare anche le credenziali Alpaca salvate?' + #13#10#13#10 + + DataDir + #13#10#13#10 + + 'Scegli No se hai intenzione di reinstallare Encelado: le credenziali ' + + 'verranno riconosciute dalla nuova installazione.', + mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then + DelTree(DataDir, True, True, True); +end; diff --git a/Encelado/build/README.md b/Encelado/build/README.md new file mode 100644 index 0000000..5c577c0 --- /dev/null +++ b/Encelado/build/README.md @@ -0,0 +1,114 @@ +# Catena di verifica, pacchetto e rilascio + +Tutto quello che serve a controllare, impacchettare e pubblicare Encelado sta in questa +cartella. La radice del progetto non contiene script. + +È la stessa catena di [Mimante/AutoBidder](http://192.168.30.23:3000/Alby96/Mimante), +adattata a una soluzione con più progetti. Le differenze sono tre, tutte segnate sul +posto in `Release.proj`: + +| | AutoBidder | Encelado | +|---|---|---| +| Dove sta la versione | `AutoBidder.csproj` | `Directory.Build.props`, ereditato da tutti i progetti | +| Cosa produce `dotnet publish` | un eseguibile unico | una cartella: l'app legge `encelado.json` accanto a sé | +| Copia portabile allegata | il solo `.exe` | uno zip della cartella | +| Cosa rigioca `Backtest` | i dossier delle aste | serie storiche di prezzi | + +| File | Cos'è | +|---|---| +| `Release.proj` | La catena. Un solo file MSBuild, nessuno script. | +| `Encelado.iss` | Lo script di Inno Setup. Non si compila a mano: lo lancia `Release.proj`. | +| `gitea.example.json` | Modello per `gitea.json` (che è escluso dal controllo di versione). | + +## Da VS Code + +**Terminale ▸ Esegui attività…** + +| Attività | Cosa fa | +|---|---| +| `verifica` | Compila e lancia i test. | +| `backtest` | Rigioca una serie storica di prezzi. | +| `crea installatore` | Chiede la versione, verifica, pubblica, esegue Inno Setup. | +| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. | +| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. | + +## Da riga di comando + +```powershell +dotnet msbuild build/Release.proj -t:Verifica +dotnet msbuild build/Release.proj -t:Pacchetto +dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.0 -p:Note="Cosa cambia" + +# Il backtest vuole un file di dati +dotnet msbuild build/Release.proj -t:Backtest -p:Dati="C:\dati\btcusd.csv" +dotnet msbuild build/Release.proj -t:Backtest -p:Dati="..." -p:Comando=frequency +``` + +| Proprietà | Predefinito | A cosa serve | +|---|---|---| +| `Versione` | vuoto | Vuoto = incrementa la minor (3.2.0 → 3.3.0). Altrimenti la scrive, se ha la forma `X.Y.Z`. | +| `Note` | vuoto | Note di rilascio. | +| `SaltaVerifica` | `false` | Non rieseguire i test. | +| `Sovrascrivi` | `false` | Sostituisci una release Gitea con lo stesso tag. | +| `Bozza` | `false` | Crea la release come bozza. | +| `ConsentiModifiche` | `false` | Tagga anche con l'albero sporco. Serve saperlo. | +| `Dati` | — | Il CSV da rigiocare. Obbligatorio per `Backtest`. | +| `Comando` | `split` | `split`, `walk`, `frequency`, `costs`, `sweep`, `explore`. | +| `Barre` | `1d` | Ampiezza delle barre nel backtest. | + +## Chi chiede la versione + +MSBuild non può chiedere niente a nessuno: è un motore di compilazione. La domanda la fa +l'attività di VS Code (`inputs` in `.vscode/tasks.json`) e passa la risposta in +`-p:Versione=`. Lasciando il campo vuoto si prende la minor successiva, che è il caso +normale di fine sessione. + +**La versione arriva dal tag e non viene scritta da nessuna parte.** `dotnet publish` la +riceve come proprietà da riga di comando, che è globale e vince su quella dichiarata in +`Directory.Build.props`. Tag, eseguibile, installatore e release portano quindi lo stesso +numero per costruzione. + +Il tag si crea **in fondo**, quando l'installatore esiste davvero. Il contrario sembra più +naturale — decidi il numero, poi costruisci — ma lascia dietro un tag quando la verifica +fallisce, e il tentativo dopo riparte da lì: il numero sale senza che sia mai esistito un +pacchetto con quella versione. + +## Gitea + +Servono quattro valori. Le variabili d'ambiente hanno la precedenza sul file, così una +macchina condivisa può rilasciare senza scrivere un token su disco: + +- `GITEA_URL`, `GITEA_OWNER`, `GITEA_REPO`, `GITEA_TOKEN` +- oppure `build/gitea.json`, copiato da `gitea.example.json` + +Il token si crea in Gitea da *Impostazioni ▸ Applicazioni ▸ Genera nuovo token*, con il +permesso `repository: read and write`. + +Il token non passa mai dalla riga di comando: sta in un file di configurazione di curl, +cancellato subito dopo il rilascio. Gli `Exec` hanno `EchoOff` perché un registro di +compilazione è la classica cosa che si incolla in una chat. + +Nella release vengono caricati **sia l'installatore sia la copia portabile**: chi non +vuole installare niente deve continuare a poter scaricare l'applicazione e basta. + +## Una trappola già pagata + +`dotnet test` e `dotnet publish` lanciati **da dentro** MSBuild ereditano l'ambiente del +processo padre. La compilazione WPF crea un progetto temporaneo (`_wpftmp.csproj`) e con +`MSBUILD_EXE_PATH` puntata al build in corso non genera più le classi parziali dello XAML: +si ottengono decine di errori su membri che esistono benissimo. + +Per questo gli `Exec` azzerano `MSBUILD_EXE_PATH` e `MSBuildLoadMicrosoftTargetsReadOnly`. +**Solo quelle due**: la ricetta che gira in rete azzera anche `MSBuildExtensionsPath` e +`MSBuildSDKsPath`, e così il figlio perde la posizione dell'SDK — *«l'SDK Microsoft.NET.Sdk +specificato non è stato trovato»*. Serve isolare il motore, non nascondergli dove abita. + +I test girano in una cartella a parte (`%TEMP%\Encelado.Verifica`) perché l'applicazione +può essere aperta mentre si lavora e tiene bloccato `Encelado.exe`: senza, la compilazione +si ferma su MSB3027. + +## Prerequisiti + +- .NET SDK 10 +- [Inno Setup 6](https://jrsoftware.org/isinfo.php) — `winget install -e --id JRSoftware.InnoSetup` +- `curl` e `git`, entrambi di serie in Windows 11 diff --git a/Encelado/build/Release.proj b/Encelado/build/Release.proj new file mode 100644 index 0000000..73e791e --- /dev/null +++ b/Encelado/build/Release.proj @@ -0,0 +1,713 @@ + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..')) + $(Radice)\src\Encelado.Bot\Encelado.Bot.csproj + $(Radice)\tests\Encelado.Tests\Encelado.Tests.csproj + $(Radice)\tools\Encelado.Backtest\Encelado.Backtest.csproj + $(MSBuildThisFileDirectory)Encelado.iss + + + $(Radice)\Directory.Build.props + + $(Radice)\bin\publish\win-x64 + $(Radice)\bin\installer + + + $([System.IO.Path]::GetTempPath())Encelado.Verifica + + + + + + $(ENCELADO_NOTE) + + + MSBUILD_EXE_PATH=;MSBuildLoadMicrosoftTargetsReadOnly= + + false + false + false + + + false + + + + + + + + + + + + + + + + + + + + + + spezza = s => + (s ?? "").Split(new[] { ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + var forma = new Regex(@"^v(\d+)\.(\d+)\.(\d+)$"); + + var suHead = new List(); + foreach (var t in spezza(TagsHead)) + if (forma.IsMatch(t.Trim())) suHead.Add(t.Trim()); + + if (suHead.Count > 1) + { + Log.LogError( + "HEAD ha piu' di un tag di versione (" + string.Join(", ", suHead) + ").\n" + + "Non si puo' sapere quale sia il rilascio: tienine uno solo con git tag -d "); + return false; + } + + var chiesta = (Richiesta ?? "").Trim(); + if (chiesta.Length > 0 && !Regex.IsMatch(chiesta, @"^\d+\.\d+\.\d+$")) + { + // Un refuso qui produrrebbe un tag e un pacchetto sbagliati. + Log.LogError("Versione '" + chiesta + "' non valida: serve la forma X.Y.Z"); + return false; + } + + if (suHead.Count == 1) + { + var dalTag = suHead[0].Substring(1); + + if (chiesta.Length > 0 && chiesta != dalTag) + { + Log.LogError( + "HEAD e' gia' taggato " + suHead[0] + ", ma e' stata chiesta la versione " + chiesta + ".\n" + + "Il tag e' la fonte: o rilasci " + dalTag + " lasciando vuota la versione,\n" + + "oppure togli il tag con git tag -d " + suHead[0] + " e rilancia."); + return false; + } + + Versione = dalTag; + Tag = suHead[0]; + DaCreare = "false"; + Log.LogMessage(MessageImportance.High, " versione " + Versione + " — dal tag " + Tag + " su HEAD"); + return true; + } + + if (chiesta.Length > 0) + { + Versione = chiesta; + } + else + { + // La minor successiva al tag piu' alto: cosi' il numero proposto e' + // sempre libero, anche se il tag piu' alto sta su un altro ramo. + int maggiore = -1, minore = -1; + foreach (var t in spezza(TagsTutti)) + { + var m = forma.Match(t.Trim()); + if (!m.Success) continue; + + int ma = int.Parse(m.Groups[1].Value), mi = int.Parse(m.Groups[2].Value); + if (ma > maggiore || (ma == maggiore && mi > minore)) { maggiore = ma; minore = mi; } + } + + if (maggiore < 0) + { + // Primo rilascio: non c'e' nessun tag da cui ripartire, e l'unico + // numero che esiste e' quello delle compilazioni di sviluppo. + var testo = File.Exists(FileVersione) ? File.ReadAllText(FileVersione) : ""; + var m = Regex.Match(testo, @"(\d+)\.(\d+)\.\d+"); + if (!m.Success) + { + Log.LogError( + "Nessun tag di versione nel repository e illeggibile in " + FileVersione + ".\n" + + "Indica la versione con -p:Versione=X.Y.Z"); + return false; + } + + maggiore = int.Parse(m.Groups[1].Value); + minore = int.Parse(m.Groups[2].Value); + } + + Versione = maggiore + "." + (minore + 1) + ".0"; + } + + Tag = "v" + Versione; + DaCreare = "true"; + Log.LogMessage(MessageImportance.High, " versione " + Versione + " — il tag " + Tag + " sara' creato a pacchetto pronto"); + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + leggi = (env, campo) => + { + var v = Environment.GetEnvironmentVariable(env); + if (!string.IsNullOrWhiteSpace(v)) return v.Trim(); + + var m = Regex.Match(testo, "\"" + campo + "\"\\s*:\\s*\"([^\"]*)\""); + return m.Success ? m.Groups[1].Value.Trim() : ""; + }; + + Url = leggi("GITEA_URL", "url").TrimEnd('/'); + Owner = leggi("GITEA_OWNER", "owner"); + Repo = leggi("GITEA_REPO", "repo"); + Token = leggi("GITEA_TOKEN", "token"); + + if (Url.Length == 0 || Owner.Length == 0 || Repo.Length == 0 || Token.Length == 0) + { + Log.LogError( + "Configurazione di Gitea incompleta. Servono url, owner, repo, token:\n" + + " copia build/gitea.example.json in build/gitea.json e riempilo,\n" + + " oppure imposta GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN.\n" + + "Non e' stato costruito niente: si controlla prima di compilare, non dopo.\n" + + "Per il solo installatore, senza Gitea, usa il target Pacchetto."); + + // Senza questo il target prosegue lo stesso: git tag, poi curl con + // l'indirizzo vuoto, e infine un "codice 3" che non dice niente a + // nessuno. Un errore va fermato dove si capisce ancora cos'era. + return false; + } + ]]> + + + + + + + + + + + + + + + + + esc = t => (t ?? "") + .Replace("\\", "\\\\").Replace("\"", "\\\"") + .Replace("\r", "").Replace("\n", "\\n").Replace("\t", " "); + + var note = string.IsNullOrWhiteSpace(Note) ? "Versione " + Versione + "." : Note; + + File.WriteAllText(Destinazione, + "{\"tag_name\":\"" + esc(Tag) + "\"," + + "\"name\":\"Encelado " + esc(Versione) + "\"," + + "\"body\":\"" + esc(note) + "\"," + + "\"draft\":" + (Bozza ? "true" : "false") + "," + + "\"prerelease\":false}"); + ]]> + + + + + + + + + + + + + + + + 200 ? testo.Substring(0, 200) : testo); + } + ]]> + + + + + + + + + + + + + + + + + + + + + split + 1d + $(Radice)\tools\Encelado.Backtest\bin\Release\net10.0\backtest.exe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(CartellaPacchetti)\Encelado_$(V).exe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(GUrl)/api/v1/repos/$(GOwner)/$(GRepo) + $([System.IO.Path]::GetTempPath())Encelado.Rilascio + $(Tmp)\curl.cfg + $(Tmp)\release.json + $(Tmp)\risposta.json + $(CartellaPacchetti)\Encelado_$(V).exe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(CartellaPacchetti)\Encelado_$(V)_portabile.zip + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Encelado/build/gitea.example.json b/Encelado/build/gitea.example.json new file mode 100644 index 0000000..16c0832 --- /dev/null +++ b/Encelado/build/gitea.example.json @@ -0,0 +1,10 @@ +{ + "_commento": "Copia questo file in gitea.json e metti il token. gitea.json e' escluso dal controllo di versione perche' contiene una credenziale. In alternativa usa le variabili d'ambiente GITEA_URL, GITEA_OWNER, GITEA_REPO, GITEA_TOKEN, che hanno la precedenza su questo file.", + + "url": "http://192.168.30.23:3000", + "owner": "Alby96", + "repo": "Encelado", + + "_token": "Gitea > Impostazioni > Applicazioni > Genera nuovo token, permesso 'repository: read and write'.", + "token": "INSERISCI_QUI_IL_TOKEN" +} diff --git a/Encelado/build/make-installer.ps1 b/Encelado/build/make-installer.ps1 deleted file mode 100644 index 45496e8..0000000 --- a/Encelado/build/make-installer.ps1 +++ /dev/null @@ -1,233 +0,0 @@ -<# -.SYNOPSIS - Costruisce Encelado-Setup-.exe, l'installer distribuibile. - -.DESCRIPTION - Tre passaggi: pubblica l'applicazione, verifica che il compilatore di Inno Setup - ci sia (e se manca lo installa), compila installer\Encelado.iss. - - Il risultato finisce in artifacts\installer\ ed è un singolo eseguibile: chi lo - riceve fa doppio clic, non gli viene chiesto nulla dall'UAC e si ritrova Encelado - nel menu Start. - -.PARAMETER FrameworkDependent - Pubblica senza il runtime .NET incorporato. L'installer scende da ~65 MB a ~2 MB, - ma sulla macchina di destinazione deve già esserci il .NET 10 Desktop Runtime, - altrimenti l'applicazione non parte e Windows mostra un errore poco chiaro. - Ha senso solo per aggiornare una macchina che hai già preparato tu. - -.PARAMETER SkipTests - Salta la suite di test prima di pubblicare. Sconsigliato: l'unica ragione per cui - i numeri della strategia sono affidabili è che qualcosa li verifica. - -.PARAMETER Configuration - Debug o Release. Il default è Release e non c'è motivo di cambiarlo. - -.EXAMPLE - .\build\make-installer.ps1 - -.EXAMPLE - .\build\make-installer.ps1 -FrameworkDependent -SkipTests -#> -[CmdletBinding()] -param( - [switch] $FrameworkDependent, - [switch] $SkipTests, - [ValidateSet('Release', 'Debug')] - [string] $Configuration = 'Release' -) - -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest - -$Root = Split-Path -Parent $PSScriptRoot -$Project = Join-Path $Root 'src\Encelado.Bot\Encelado.Bot.csproj' -$Solution = Join-Path $Root 'Encelado.slnx' -$IssScript = Join-Path $Root 'installer\Encelado.iss' -$PublishDir = Join-Path $Root 'artifacts\publish' -$OutputDir = Join-Path $Root 'artifacts\installer' - -function Write-Step([string] $Message) { - Write-Host '' - Write-Host "==> $Message" -ForegroundColor Cyan -} - -function Write-Note([string] $Message) { - Write-Host " $Message" -ForegroundColor DarkGray -} - -# -------------------------------------------------------------------------------- -# Versione -# -------------------------------------------------------------------------------- -# Una sola fonte di verità: Directory.Build.props. Se la versione fosse scritta anche -# nello script .iss, prima o poi le due divergerebbero e verrebbero distribuiti due -# installer diversi con lo stesso numero sopra. - -function Get-ProductVersion { - $propsPath = Join-Path $Root 'Directory.Build.props' - if (-not (Test-Path -LiteralPath $propsPath)) { - throw "Directory.Build.props non trovato in $Root." - } - - $version = ([xml](Get-Content -LiteralPath $propsPath -Raw)). - SelectSingleNode('//PropertyGroup/Version') - - if ($null -eq $version -or [string]::IsNullOrWhiteSpace($version.InnerText)) { - throw "Nessun elemento in $propsPath." - } - - $text = $version.InnerText.Trim() - if ($text -notmatch '^\d+(\.\d+){1,3}$') { - throw "Versione '$text' non utilizzabile: Inno Setup vuole da 2 a 4 numeri separati da punti." - } - - return $text -} - -# -------------------------------------------------------------------------------- -# Compilatore Inno Setup -# -------------------------------------------------------------------------------- - -function Find-Iscc { - $command = Get-Command 'ISCC.exe' -ErrorAction SilentlyContinue - if ($null -ne $command) { return $command.Source } - - # Inno registra qui la propria cartella; è più affidabile di indovinare il percorso. - $keys = @( - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1', - 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1', - 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1' - ) - - foreach ($key in $keys) { - try { - $location = (Get-ItemProperty -LiteralPath $key -ErrorAction Stop).InstallLocation - } catch { - continue - } - - if ([string]::IsNullOrWhiteSpace($location)) { continue } - - $candidate = Join-Path $location 'ISCC.exe' - if (Test-Path -LiteralPath $candidate) { return $candidate } - } - - foreach ($candidate in @( - "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", - "$env:ProgramFiles\Inno Setup 6\ISCC.exe", - "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe")) { - - if (Test-Path -LiteralPath $candidate) { return $candidate } - } - - return $null -} - -function Install-InnoSetup { - if ($null -eq (Get-Command 'winget' -ErrorAction SilentlyContinue)) { - throw @' -Inno Setup non è installato e winget non è disponibile per installarlo. -Scaricalo da https://jrsoftware.org/isdl.php e rilancia questo script. -'@ - } - - Write-Note 'Inno Setup non trovato: lo installo con winget (una tantum).' - & winget install --id JRSoftware.InnoSetup --exact --silent ` - --accept-package-agreements --accept-source-agreements - - # winget restituisce 0 anche in casi che non ci interessano, quindi la vera - # verifica è ricercare l'eseguibile. - $iscc = Find-Iscc - if ($null -eq $iscc) { - throw 'Installazione di Inno Setup non riuscita. Installalo a mano da https://jrsoftware.org/isdl.php' - } - - return $iscc -} - -# -------------------------------------------------------------------------------- - -$version = Get-ProductVersion -Write-Host '' -Write-Host "Encelado $version — costruzione dell'installer" -ForegroundColor White - -if (-not $SkipTests) { - Write-Step 'Test' - & dotnet test $Solution --configuration $Configuration --nologo --verbosity quiet - if ($LASTEXITCODE -ne 0) { - throw 'Test non superati: mi fermo qui invece di impacchettare qualcosa di rotto.' - } -} - -Write-Step 'Pubblicazione' - -# Una cartella pulita a ogni giro. Senza questo, i resti di una pubblicazione -# precedente — una DLL rinominata, un runtime cambiato — finirebbero dentro -# l'installer, e sarebbero il tipo di problema che si manifesta solo sulla macchina -# di qualcun altro. -if (Test-Path -LiteralPath $PublishDir) { - Remove-Item -LiteralPath $PublishDir -Recurse -Force -} - -$publishArgs = @( - 'publish', $Project, - '--configuration', $Configuration, - '--runtime', 'win-x64', - '--output', $PublishDir, - '--nologo', - "-p:SelfContained=$(if ($FrameworkDependent) { 'false' } else { 'true' })", - '-p:DebugType=none', - '-p:GenerateDocumentationFile=false' -) - -if ($FrameworkDependent) { - Write-Note 'Senza runtime incorporato: sulla macchina di destinazione serve il .NET 10 Desktop Runtime.' -} else { - Write-Note 'Con runtime incorporato: nessun prerequisito sulla macchina di destinazione.' -} - -& dotnet @publishArgs -if ($LASTEXITCODE -ne 0) { throw 'Pubblicazione non riuscita.' } - -$exePath = Join-Path $PublishDir 'Encelado.exe' -if (-not (Test-Path -LiteralPath $exePath)) { - throw "La pubblicazione è andata a buon fine ma Encelado.exe non c'è in $PublishDir." -} - -$configPath = Join-Path $PublishDir 'encelado.json' -if (-not (Test-Path -LiteralPath $configPath)) { - throw "encelado.json manca dal publish: senza configurazione l'applicazione non parte." -} - -$publishSize = (Get-ChildItem -LiteralPath $PublishDir -Recurse -File | - Measure-Object -Property Length -Sum).Sum -Write-Note ('{0:N0} file, {1:N1} MB' -f ` - (Get-ChildItem -LiteralPath $PublishDir -Recurse -File).Count, ($publishSize / 1MB)) - -Write-Step 'Installer' - -$iscc = Find-Iscc -if ($null -eq $iscc) { - $iscc = Install-InnoSetup -} -Write-Note $iscc - -New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null - -& $iscc $IssScript "/DAppVersion=$version" "/DPublishDir=$PublishDir" '/Qp' -if ($LASTEXITCODE -ne 0) { throw 'Compilazione dell''installer non riuscita.' } - -$setup = Join-Path $OutputDir "Encelado-Setup-$version.exe" -if (-not (Test-Path -LiteralPath $setup)) { - throw "Inno Setup non ha segnalato errori ma $setup non esiste." -} - -$setupSize = (Get-Item -LiteralPath $setup).Length - -Write-Host '' -Write-Host 'Fatto.' -ForegroundColor Green -Write-Host " $setup" -Write-Host (' {0:N1} MB' -f ($setupSize / 1MB)) -Write-Host '' -Write-Host ' Si installa in %LOCALAPPDATA%\Programs\Encelado senza richiesta di privilegi.' -ForegroundColor DarkGray -Write-Host ' Un encelado.json già presente non viene sovrascritto.' -ForegroundColor DarkGray diff --git a/Encelado/installer/Encelado.iss b/Encelado/installer/Encelado.iss deleted file mode 100644 index d8f148f..0000000 --- a/Encelado/installer/Encelado.iss +++ /dev/null @@ -1,141 +0,0 @@ -; Encelado — script di installazione (Inno Setup 6). -; -; Non compilarlo a mano: usa build\make-installer.ps1 (o il task "installer" di VS -; Code), che pubblica l'applicazione, ricava la versione da Directory.Build.props e -; passa qui sotto i due #define che mancano. -; -; ISCC.exe installer\Encelado.iss /DAppVersion=2.0.0 /DPublishDir=...\publish -; -; Scelta di fondo: l'installazione è PER UTENTE, in %LOCALAPPDATA%\Programs\Encelado. -; Non è una semplificazione per evitare l'UAC. Encelado scrive i log, il diario -; operazioni e i CSV di analisi accanto al proprio eseguibile (LoggingOptions. -; ResolveDirectory risolve i percorsi relativi su AppContext.BaseDirectory): dentro -; C:\Program Files quelle scritture fallirebbero silenziosamente e l'utente si -; ritroverebbe senza i log proprio quando gli servono per capire cosa ha fatto il bot. -; Per questo PrivilegesRequired resta "lowest" e non è consentito forzarlo. - -#ifndef AppVersion - #error AppVersion non definito: lancia build\make-installer.ps1 -#endif - -#ifndef PublishDir - #error PublishDir non definito: lancia build\make-installer.ps1 -#endif - -#define AppName "Encelado" -#define AppPublisher "Encelado" -#define AppExeName "Encelado.exe" -#define AppDescription "Bot di trading automatico su Alpaca" - -[Setup] -; Questo GUID identifica il prodotto per sempre: cambiarlo significa che un -; aggiornamento verrà installato di fianco al vecchio invece che sopra. -AppId={{7C4F1E62-2B8A-4D19-9C55-3E0A6B1D8F44} -AppName={#AppName} -AppVersion={#AppVersion} -AppVerName={#AppName} {#AppVersion} -VersionInfoVersion={#AppVersion} -VersionInfoDescription={#AppDescription} -AppPublisher={#AppPublisher} -UninstallDisplayName={#AppName} {#AppVersion} -UninstallDisplayIcon={app}\{#AppExeName} - -DefaultDirName={autopf}\{#AppName} -DefaultGroupName={#AppName} -DisableProgramGroupPage=yes -DisableDirPage=auto - -; Vedi la nota in testa al file: l'app deve poter scrivere nella propria cartella. -PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed= - -; Il publish è win-x64. -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible - -OutputDir={#SourcePath}\..\artifacts\installer -OutputBaseFilename={#AppName}-Setup-{#AppVersion} -SetupIconFile={#SourcePath}\..\src\Encelado.Bot\Assets\encelado.ico -WizardStyle=modern -Compression=lzma2/max -SolidCompression=yes - -; Se Encelado è in esecuzione, il Restart Manager lo chiude invece di lasciare -; l'installazione a metà con i file bloccati. -CloseApplications=yes -RestartApplications=no - -[Languages] -Name: "it"; MessagesFile: "compiler:Languages\Italian.isl" -Name: "en"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" - -[Files] -; Tutto il publish tranne la configurazione, che ha una regola sua, e i simboli di -; debug, che non servono a chi installa. -Source: "{#PublishDir}\*"; DestDir: "{app}"; \ - Excludes: "encelado.json,*.pdb,*.xml,logs\*"; \ - Flags: ignoreversion recursesubdirs createallsubdirs - -; La configurazione è il prodotto — ogni numero dentro encelado.json è tarato su nove -; anni di backtest — ma è anche l'unico posto dove l'utente mette mano (verbosità dei -; log, rischio per operazione). "onlyifdoesntexist" fa sì che un aggiornamento non -; cancelli quelle modifiche; "uninsneveruninstall" che una disinstallazione non le -; butti via. Le chiavi nuove introdotte da una versione successiva non rompono nulla: -; il loader usa i valori di default per quelle che non trova. -Source: "{#PublishDir}\encelado.json"; DestDir: "{app}"; \ - Flags: onlyifdoesntexist uninsneveruninstall - -; Copia sempre aggiornata dei valori di fabbrica, per poter vedere cosa è cambiato -; rispetto al proprio encelado.json dopo un aggiornamento. -Source: "{#PublishDir}\encelado.json"; DestDir: "{app}"; \ - DestName: "encelado.default.json"; Flags: ignoreversion - -[Icons] -Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Comment: "{#AppDescription}" -Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}" -Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; \ - Comment: "{#AppDescription}"; Tasks: desktopicon - -[Run] -Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#AppName}}"; \ - Flags: nowait postinstall skipifsilent - -[UninstallDelete] -; Prodotti a runtime, quindi non tracciati dall'installer: senza questo resterebbero -; una cartella vuota e dei file orfani. -Type: filesandordirs; Name: "{app}\logs" - -[Code] -{ Le credenziali Alpaca vivono in %LOCALAPPDATA%\Encelado, fuori dalla cartella di - installazione, quindi una disinstallazione normale non le toccherebbe. Lasciarle lì - in silenzio però significa lasciare sul disco una chiave API cifrata di cui l'utente - si è dimenticato. Glielo chiediamo, con il "no" come risposta predefinita: chi - disinstalla per reinstallare una versione nuova non deve ritrovarsi a reinserire le - chiavi solo perché ha premuto Invio di fretta. } -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - DataDir: String; -begin - if CurUninstallStep <> usPostUninstall then - Exit; - - { In modalità silenziosa non c'è nessuno a cui chiedere, e la risposta che non si - può disfare è quella che cancella. Nel dubbio le credenziali restano dove sono. } - if UninstallSilent then - Exit; - - DataDir := ExpandConstant('{localappdata}\Encelado'); - if not DirExists(DataDir) then - Exit; - - if MsgBox( - 'Vuoi eliminare anche le credenziali Alpaca salvate?' + #13#10#13#10 + - DataDir + #13#10#13#10 + - 'Scegli No se hai intenzione di reinstallare Encelado: le credenziali ' - + 'verranno riconosciute dalla nuova installazione.', - mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then - DelTree(DataDir, True, True, True); -end; diff --git a/README.md b/README.md index 54c11ef..5b3e273 100644 --- a/README.md +++ b/README.md @@ -45,49 +45,66 @@ prima — niente variabili d'ambiente, niente file da editare. --- -## Installer +## Verifica, pacchetto e rilascio -Per portare Encelado su un'altra macchina serve un file solo: +**È la stessa catena di [Mimante/AutoBidder](http://192.168.30.23:3000/Alby96/Mimante): +un solo file MSBuild in `build/Release.proj`, nessuno script.** I dettagli stanno in +[`build/README.md`](build/README.md). + +Da VS Code: **Terminale ▸ Esegui attività…** + +| Attività | Cosa fa | +|---|---| +| `verifica` | Compila e lancia i test. | +| `backtest` | Rigioca una serie storica di prezzi. | +| `crea installatore` | Verifica, pubblica, esegue Inno Setup, crea il tag. | +| `crea installatore (senza rieseguire i test)` | Solo pubblicazione e installatore. | +| `rilascia su Gitea` | Tutto quanto sopra, più tag e release con i file allegati. | ```powershell -.\build\make-installer.ps1 +dotnet msbuild build/Release.proj -t:Verifica +dotnet msbuild build/Release.proj -t:Pacchetto +dotnet msbuild build/Release.proj -t:Rilascia -p:Versione=3.3.0 ``` -Da VS Code: **Ctrl+Shift+P → Run Task → installer**. +**La versione viene dal tag git, e da nient'altro.** Non viene mai riscritto un file: +il numero arriva a `dotnet publish` come proprietà da riga di comando, quindi tag, +eseguibile, installatore e release portano lo stesso numero per costruzione, non per +disciplina. Il `` in `Directory.Build.props` resta quello delle compilazioni +di sviluppo — è il numero che vedi nella finestra mentre lavori — e serve solo come +seme al primissimo rilascio, quando non esiste ancora nessun tag. -Lo script esegue i test, pubblica in `win-x64` e compila `installer\Encelado.iss`, -lasciando `artifacts\installer\Encelado-Setup-.exe` — circa 45 MB. Al primo -utilizzo installa Inno Setup con `winget` se non lo trova; dopo non serve più. +Il tag viene creato **in fondo**, a installatore esistente: un giro andato male non +lascia dietro un tag per una versione che non è mai stata costruita. E non viene creato +affatto se ci sono modifiche non committate, perché indicherebbe uno stato non +ricostruibile. -Chi riceve quel file ci fa doppio clic e basta: nessuna richiesta di privilegi, -nessun prerequisito da installare prima. +Il pacchetto esce in `bin/installer/`: | | | |---|---| +| `Encelado--setup.exe` | L'installatore, ~47 MB | +| `Encelado--portabile.zip` | La cartella pubblicata, per chi non vuole installare niente | | Destinazione | `%LOCALAPPDATA%\Programs\Encelado` | | UAC | nessuna richiesta | | Runtime .NET | incorporato, non serve installarlo | -| Menu Start e desktop | collegamenti creati, disinstallazione dal pannello di controllo | -**Perché per utente e non in `Programs Files`.** Non è per evitare l'UAC. Encelado +**Perché per utente e non in `Program Files`.** Non è per evitare l'UAC. Encelado scrive log, diario operazioni e CSV di analisi accanto al proprio eseguibile: dentro `C:\Program Files` quelle scritture fallirebbero, e siccome il logger degrada in silenzio piuttosto che bloccare il bot, te ne accorgeresti solo cercando i log per capire cosa è successo. Per questo `PrivilegesRequired=lowest` non è sovrascrivibile. **Aggiornamenti.** Reinstallare sopra una versione precedente **non** tocca il tuo -`encelado.json`: le modifiche a rischio e verbosità sopravvivono. Accanto trovi -sempre `encelado.default.json` aggiornato, per confrontare la tua configurazione con -i valori di fabbrica. Se l'applicazione è in esecuzione, l'installer la chiude. +`encelado.json`: le modifiche fatte dalla scheda Impostazioni sopravvivono. Accanto +trovi sempre `encelado.default.json` aggiornato, per confrontare la tua configurazione +con i valori di fabbrica. Se l'applicazione è in esecuzione, l'installatore la chiude. **Disinstallazione.** Rimuove programma e log. Le credenziali Alpaca stanno altrove (`%LOCALAPPDATA%\Encelado`) e vengono cancellate solo se rispondi Sì alla domanda esplicita — il default è No, così reinstallare non costringe a reinserire le chiavi. In modalità silenziosa la domanda non viene posta e le credenziali restano. -Con `-FrameworkDependent` l'installer scende a ~2 MB, ma sulla macchina di -destinazione deve già esserci il .NET 10 Desktop Runtime. - --- ## Cosa c'è nella finestra @@ -549,15 +566,15 @@ src/ Theme.xaml tavolozza, stili e template dei controlli Assets/ icona multi-risoluzione (16→256 px) tests/ - Encelado.Tests/ 250 test + Encelado.Tests/ 309 test tools/ Encelado.Backtest/ banco di prova, non fa parte dell'applicazione config/ encelado.json la configurazione consegnata build/ - make-installer.ps1 pubblica e impacchetta -installer/ - Encelado.iss script Inno Setup + Release.proj verifica, pacchetto e rilascio — un file MSBuild, nessuno script + Encelado.iss script Inno Setup, lanciato da Release.proj + gitea.example.json modello per gitea.json, che resta fuori dal repository ``` ### Il banco di prova