Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 110 additions & 20 deletions desktop/latex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
Expand Down
Loading