diff --git a/desktop/latex.js b/desktop/latex.js index 885198e..ac6cc5f 100644 --- a/desktop/latex.js +++ b/desktop/latex.js @@ -50,31 +50,35 @@ export const COLLEZIONI_COMPLETE = [ const PIATTAFORMA = process.platform; // 'win32' | 'darwin' | 'linux' -// URL ufficiali di TinyTeX (distribuzione TeX Live minima e installabile -// senza privilegi). Ne teniamo più di uno per piattaforma perché sia il -// redirect di yihui.org sia i nomi dei file sulle release GitHub sono -// cambiati più volte nel tempo: se il primo risponde 404 si passa al -// successivo invece di far fallire subito l'installazione. -// Sovrascrivibile con TEXFORGE_TINYTEX_URL per usare un mirror o una copia -// locale (in tal caso viene provato solo quell'URL). -const TINYTEX_URL = { +// TinyTeX (distribuzione TeX Live minima, installabile senza privilegi) usa +// un'estensione diversa per piattaforma. +const ESTENSIONE_ARCHIVIO = { + win32: '.zip', + darwin: '.tgz', + linux: '.tar.gz', +}; + +// URL di riserva, usati solo se l'elenco delle release non è raggiungibile. +// I nomi dei file sulle release di tinytex-releases sono cambiati più volte +// (oggi portano il numero di versione, es. TinyTeX-1-v2025.08.zip), perciò +// non ci affidiamo a questi URL fissi: sono l'ultima spiaggia, non la regola. +const TINYTEX_URL_RISERVA = { win32: [ - 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.zip', - 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.zip', 'https://yihui.org/tinytex/TinyTeX-1.zip', + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.zip', ], darwin: [ - 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tgz', - 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.tgz', 'https://yihui.org/tinytex/TinyTeX-1.tgz', + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tgz', ], linux: [ - 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tar.gz', - 'https://github.com/rstudio/tinytex-releases/releases/latest/download/TinyTeX-1.tar.gz', 'https://yihui.org/tinytex/TinyTeX-1.tar.gz', + 'https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1.tar.gz', ], }; +const API_RELEASES = 'https://api.github.com/repos/rstudio/tinytex-releases/releases?per_page=10'; + // --------------------------------------------------------------------------- // Percorsi // --------------------------------------------------------------------------- @@ -207,6 +211,79 @@ function scarica(url, destinazione, onProgress) { }); } +// Scarica una risorsa piccola (JSON) tenendola in memoria, seguendo i redirect. +function scaricaTesto(url, redirectRimasti = 5) { + return new Promise((resolve, reject) => { + const richiesta = https.get(url, { + headers: { 'User-Agent': 'texforge', Accept: 'application/vnd.github+json' }, + }, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + res.resume(); + if (redirectRimasti <= 0) return reject(new Error('troppi redirect')); + return scaricaTesto(res.headers.location, redirectRimasti - 1).then(resolve, reject); + } + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`richiesta fallita: HTTP ${res.statusCode}`)); + } + let corpo = ''; + res.setEncoding('utf8'); + res.on('data', (c) => { corpo += c; }); + res.on('end', () => resolve(corpo)); + }); + richiesta.on('error', reject); + richiesta.setTimeout(30000, () => richiesta.destroy(new Error('timeout della richiesta'))); + }); +} + +// Fra gli allegati di una release scegliamo TinyTeX-1 (l'installazione +// "media": più di TinyTeX-0 minimale, meno di TinyTeX-2 completa), accettando +// sia il nome nudo sia quello con la versione (TinyTeX-1-v2025.08.zip). +function ordineVarianteTinyTeX(nome, estensione) { + const base = nome.slice(0, nome.length - estensione.length); + if (/^TinyTeX-1(-v.+)?$/.test(base)) return 0; + if (/^TinyTeX(-v.+)?$/.test(base)) return 1; + if (/^TinyTeX-2(-v.+)?$/.test(base)) return 2; + if (/^TinyTeX-0(-v.+)?$/.test(base)) return 3; + return -1; +} + +// Ricava dagli allegati delle release più recenti gli URL da provare. Così un +// nuovo schema di nomi (o una release senza il tag "daily") non rompe più +// l'installazione: leggiamo i nomi veri invece di indovinarli. +async function urlDalleRelease(estensione, onLog) { + const release = JSON.parse(await scaricaTesto(API_RELEASES)); + if (!Array.isArray(release)) throw new Error('risposta inattesa dall\'API di GitHub'); + + const candidati = []; + for (const r of release) { + if (r?.draft) continue; + const allegati = (r.assets || []) + .filter((a) => typeof a.name === 'string' && a.name.endsWith(estensione)) + .map((a) => ({ url: a.browser_download_url, ordine: ordineVarianteTinyTeX(a.name, estensione) })) + .filter((a) => a.url && a.ordine >= 0) + .sort((a, b) => a.ordine - b.ordine); + for (const a of allegati) candidati.push(a.url); + } + if (!candidati.length) throw new Error('nessun archivio TinyTeX nelle release'); + onLog?.(`Trovate ${candidati.length} versioni di TinyTeX sulle release ufficiali.\n`); + return candidati; +} + +// Elenco completo degli URL da provare, dal più affidabile al meno. +async function risolviCandidati(estensione, onLog) { + if (process.env.TEXFORGE_TINYTEX_URL) return [process.env.TEXFORGE_TINYTEX_URL]; + + let dallApi = []; + try { + dallApi = await urlDalleRelease(estensione, onLog); + } catch (e) { + onLog?.(`Non riesco a leggere l'elenco delle release (${e.message}), uso gli indirizzi noti...\n`); + } + const riserva = TINYTEX_URL_RISERVA[PIATTAFORMA] || []; + return [...new Set([...dallApi, ...riserva])]; +} + function eseguiConLog(comando, argomenti, opzioni, onLog) { return new Promise((resolve, reject) => { const proc = spawn(comando, argomenti, { ...opzioni, windowsHide: true }); @@ -252,25 +329,38 @@ async function scaricaConFallback(candidati, archivio, onLog, onProgress) { try { onLog?.(`Scarico la distribuzione TeX da ${url}\n`); await scarica(url, archivio, onProgress); + // Un archivio di pochi kB non è TinyTeX: di solito è una pagina di + // errore servita con codice 200. Meglio accorgersene qui che a + // estrazione fallita. + const { size } = await fsp.stat(archivio); + if (size < 1024 * 1024) throw new Error(`archivio troppo piccolo (${size} byte)`); return; } catch (e) { ultimoErrore = e; + await fsp.rm(archivio, { force: true }).catch(() => {}); onLog?.(`Fonte non disponibile (${e.message}), provo un'altra fonte...\n`); } } - throw ultimoErrore; + throw new Error( + `impossibile scaricare TinyTeX (${ultimoErrore?.message || 'nessuna fonte disponibile'}). ` + + 'Controlla la connessione, oppure imposta TEXFORGE_TINYTEX_URL con l\'indirizzo ' + + 'di un archivio TinyTeX scaricato a mano.', + ); } // Scarica e installa TinyTeX nella cartella dati dell'utente. export async function installaDistribuzione(userDataDir, { onLog, onProgress } = {}) { - const candidati = process.env.TEXFORGE_TINYTEX_URL - ? [process.env.TEXFORGE_TINYTEX_URL] - : TINYTEX_URL[PIATTAFORMA]; - if (!candidati) throw new Error(`piattaforma non supportata: ${PIATTAFORMA}`); + const estensione = ESTENSIONE_ARCHIVIO[PIATTAFORMA]; + if (!estensione) throw new Error(`piattaforma non supportata: ${PIATTAFORMA}`); + + const candidati = await risolviCandidati(estensione, onLog); + if (!candidati.length) throw new Error('nessuna fonte disponibile per TinyTeX'); const radice = radiceGestita(userDataDir); const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), 'texforge-tex-')); - const archivio = path.join(tmp, path.basename(new URL(candidati[0]).pathname)); + // Il nome locale segue la piattaforma, non l'URL: è quello che decide come + // estrarre l'archivio, e gli URL delle release cambiano nome. + const archivio = path.join(tmp, `TinyTeX${estensione}`); try { await scaricaConFallback(candidati, archivio, onLog, onProgress);