Ampel / ampel (push) Successful in 1m22s
electron-builder → NSIS: EIN RippySetup-<version>.exe, pro Benutzer, ohne Adminrechte, eigenes Icon (PNG-basiertes ICO selbst gebaut). extraResources: die MITGELIEFERTEN Werkzeuge (HandBrakeCLI GPL-2, flac.exe MIT libFLAC.dll — die Ohne-DLL-Falle aus § 9) samt Lizenztexten und Quellverweisen (bau/lizenzen, Entscheid 9); vendor/ liegt nicht im Git (74 MB), Befuellung steht in BAUEN.md. Der Kern findet die mitgelieferten Werkzeuge ueber RIPPY_VENDOR (resourcesvendor) VOR allen anderen Suchwegen. haupt/update.ts (§ 6.8): electron-updater gegen das oeffentliche Gitea (festes Tag 'aktuell' als Rueckfallebene, Entscheid 6) — NUR https (http wird ABGELEHNT; Regel-Tests), NIE waehrend eines Rips (der Haupt kennt die aktiven Vorgaenge), Fehlschlaege still aber ablesbar (letzte erfolgreiche Pruefung im Status), installiert beim naechsten Start. haupt/autostart.ts: HKCU-Run ueber app.setLoginItemSettings, Toggle in den Einstellungen. Erst-Einrichtung (§ 6.9) im Programm: 5 Schritte, NUR ein Fehler blockiert (kein Laufwerk = Warnung, MakeMKV-Pflicht mit Klartext-Weg). GEBAUT UND GEMESSEN (30.08.2026): - dist-setup/RippySetup-5.0.0-w0.exe — 130,8 MB, SHA-256 07D39D6BAB74A47C21AA53D847FB5A5DB6990AE9FA1CBC7BE814B51050883115, plus .blockmap. - MESSUNG: Die Update-Auskunft heisst bei Vorab-Versionen nach dem KANAL (w0.yml) — erst ein Release ohne Suffix erzeugt latest.yml (steht jetzt in BAUEN.md). - Das GEPACKTE Programm (win-unpacked, asar + resources) besteht den Smoke: SMOKE OK, und der Screenshot zeigt die Erst-Einrichtung — das Bild eines frischen Rechners. Bewusst offen (SAVEPOINT): die automatische MakeMKV-Beschaffung (beschaffen.ts — Download+Installer-Start braucht einen Live-Test); das Setup NENNT den Weg klar. 185 Tests gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
204 lines
7.5 KiB
TypeScript
204 lines
7.5 KiB
TypeScript
// Wo Rippys Werkzeuge liegen — und wie man sie findet (KONZEPT § 9).
|
|
// Die Suchreihenfolge und ihre Fallen stammen aus tools/katalog.py, dort am
|
|
// Commander-PC gemessen:
|
|
//
|
|
// 1. Ausdrücklich eingestellt — wer einen Pfad einträgt, will genau den.
|
|
// 2. Rippys eigener Werkzeug-Ordner (mitgeliefert bzw. selbst geholt) —
|
|
// VOR dem System, damit eine gepflegte Fassung eine alte Installation
|
|
// schlägt.
|
|
// 3. PATH — der „selbst eingetragen"-Fall.
|
|
// 4. Bekannte Installationsorte (%ProgramFiles%…) — der Normalfall.
|
|
// 5. Uninstall-Zweig der Registry: Bei MakeMKV ist `InstallLocation` LEER
|
|
// (28.08.2026 nachgesehen), aber `DisplayIcon` zeigt in den
|
|
// Programmordner.
|
|
//
|
|
// ⚠ %VAR%-Entfaltung MUSS Groß/Klein ignorieren: os-Umgebungen liefern
|
|
// PROGRAMFILES(X86), das Muster sagt %ProgramFiles(x86)% — der strikte
|
|
// Vergleich ließ die bekannten Orte auf einer echten Maschine NIE greifen
|
|
// (katalog.py, 28.08.2026).
|
|
import { spawnSync } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
// Ausdrücklich die WINDOWS-Pfadregeln: Rippy v5 baut nur Windows-Pfade,
|
|
// und die CI-Tests laufen auf Linux — dort würde das plattformabhängige
|
|
// join() Schrägstriche hineinmischen (genau daran brach Ampel-Lauf 226).
|
|
import { win32 as pfad } from 'node:path'
|
|
|
|
const { delimiter, join } = pfad
|
|
|
|
export interface Werkzeug {
|
|
titel: string
|
|
zweck: string
|
|
programme: readonly string[]
|
|
orte: readonly string[]
|
|
/** Teilstring des DisplayName im Uninstall-Zweig der Registry. */
|
|
anzeige: string
|
|
pflicht: boolean
|
|
}
|
|
|
|
export const WERKZEUGE: Record<string, Werkzeug> = {
|
|
makemkv: {
|
|
titel: 'MakeMKV',
|
|
zweck: 'Liest DVDs, Blu-rays und 4K-UHDs — der einzige Weg durch AACS.',
|
|
programme: ['makemkvcon64.exe', 'makemkvcon.exe'],
|
|
orte: ['%ProgramFiles(x86)%\\MakeMKV', '%ProgramFiles%\\MakeMKV'],
|
|
anzeige: 'MakeMKV',
|
|
pflicht: true,
|
|
},
|
|
handbrake: {
|
|
titel: 'HandBrakeCLI',
|
|
zweck: 'Komprimiert den verlustfreien Rip auf Arbeitsgröße.',
|
|
programme: ['HandBrakeCLI.exe'],
|
|
orte: ['%ProgramFiles%\\HandBrake', '%ProgramFiles(x86)%\\HandBrake'],
|
|
anzeige: 'HandBrake',
|
|
pflicht: false, // ohne HandBrake geht Rippen, nur ohne Kompression
|
|
},
|
|
flac: {
|
|
titel: 'FLAC',
|
|
zweck: 'Wandelt die von der Audio-CD gelesenen Rohdaten in FLAC.',
|
|
programme: ['flac.exe'],
|
|
orte: ['%ProgramFiles%\\FLAC', '%ProgramFiles(x86)%\\FLAC'],
|
|
anzeige: 'FLAC',
|
|
pflicht: false, // ohne FLAC läuft alles außer Audio-CDs
|
|
},
|
|
}
|
|
|
|
/** Rippys eigener Werkzeug-Ordner — dort landet Mitgeliefertes/Geholtes. */
|
|
export function werkzeugOrdner(umgebung: NodeJS.ProcessEnv = process.env): string {
|
|
const wurzel = umgebung['LOCALAPPDATA'] ?? umgebung['USERPROFILE'] ?? '.'
|
|
return join(wurzel, 'Rippy', 'tools')
|
|
}
|
|
|
|
/** %ProgramFiles%\X → C:\Program Files\X — Groß/Klein wird IGNORIERT.
|
|
* Bleibt ein % stehen, ist der Kandidat unbrauchbar → ''. */
|
|
export function entfalten(pfad: string, umgebung: NodeJS.ProcessEnv): string {
|
|
let ergebnis = pfad
|
|
for (const [name, wert] of Object.entries(umgebung)) {
|
|
if (wert === undefined) continue
|
|
const marke = `%${name}%`.toUpperCase()
|
|
for (;;) {
|
|
const stelle = ergebnis.toUpperCase().indexOf(marke)
|
|
if (stelle < 0) break
|
|
ergebnis = ergebnis.slice(0, stelle) + wert + ergebnis.slice(stelle + marke.length)
|
|
}
|
|
}
|
|
return ergebnis.includes('%') ? '' : ergebnis
|
|
}
|
|
|
|
/** Alle Kandidaten-Pfade in Suchreihenfolge — reine Funktion, prüft nichts. */
|
|
export function kandidaten(
|
|
name: string,
|
|
eigenerOrdner: string,
|
|
umgebung: NodeJS.ProcessEnv,
|
|
): string[] {
|
|
const werkzeug = WERKZEUGE[name]
|
|
const pfade: string[] = []
|
|
// Das MITGELIEFERTE Werkzeug (§ 9) — im installierten Rippy zeigt
|
|
// RIPPY_VENDOR auf resources\vendor (der Haupt setzt es beim Kern-Start).
|
|
const vendor = umgebung['RIPPY_VENDOR'] ?? ''
|
|
if (vendor.length > 0) {
|
|
for (const programm of werkzeug.programme) {
|
|
pfade.push(join(vendor, name, programm))
|
|
}
|
|
}
|
|
for (const programm of werkzeug.programme) {
|
|
pfade.push(join(eigenerOrdner, name, programm))
|
|
pfade.push(join(eigenerOrdner, programm))
|
|
}
|
|
for (const ort of werkzeug.orte) {
|
|
const entfaltet = entfalten(ort, umgebung)
|
|
if (entfaltet.length === 0) continue
|
|
for (const programm of werkzeug.programme) {
|
|
pfade.push(join(entfaltet, programm))
|
|
}
|
|
}
|
|
return pfade
|
|
}
|
|
|
|
function imPfadSuchen(programme: readonly string[], umgebung: NodeJS.ProcessEnv): string {
|
|
const pfadVar = umgebung['PATH'] ?? umgebung['Path'] ?? ''
|
|
for (const ordner of pfadVar.split(delimiter)) {
|
|
if (ordner.length === 0) continue
|
|
for (const programm of programme) {
|
|
const kandidat = join(ordner, programm)
|
|
if (existsSync(kandidat)) return kandidat
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
/** Registry-Rückfall: DisplayIcon aus dem Uninstall-Zweig. Bei MakeMKV ist
|
|
* InstallLocation leer — DisplayIcon zeigt auf eine Datei im
|
|
* Programmordner (28.08.2026 nachgesehen). Über reg.exe gelesen, damit
|
|
* kein zweiter koffi-Ort entsteht (R1). */
|
|
export function ausUninstallRegistry(
|
|
anzeige: string,
|
|
programme: readonly string[],
|
|
ausfuehren: (wurzel: string) => string = regQuery,
|
|
): string {
|
|
const wurzeln = [
|
|
'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
]
|
|
for (const wurzel of wurzeln) {
|
|
const ausgabe = ausfuehren(wurzel)
|
|
// reg.exe gruppiert je Unterschlüssel: Kopfzeile, dann NAME TYP WERT.
|
|
let passt = false
|
|
for (const zeile of ausgabe.split(/\r?\n/)) {
|
|
if (zeile.startsWith('HK')) {
|
|
passt = false
|
|
continue
|
|
}
|
|
const wert = /^\s*(\S+)\s+REG_SZ\s+(.+)$/.exec(zeile)
|
|
if (wert === null) continue
|
|
if (wert[1] === 'DisplayName') {
|
|
passt = wert[2].includes(anzeige)
|
|
} else if (passt && (wert[1] === 'DisplayIcon' || wert[1] === 'UninstallString')) {
|
|
const ordner = wert[2].replace(/^"|"$/g, '').replace(/[^\\]*$/, '')
|
|
for (const programm of programme) {
|
|
const kandidat = join(ordner, programm)
|
|
if (existsSync(kandidat)) return kandidat
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function regQuery(wurzel: string): string {
|
|
const ergebnis = spawnSync('reg', ['query', wurzel, '/s'], {
|
|
encoding: 'utf8',
|
|
windowsHide: true,
|
|
timeout: 15_000,
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
})
|
|
return ergebnis.stdout ?? ''
|
|
}
|
|
|
|
/** Der Pfad zum Werkzeug — oder '' wenn es nicht da ist. Prüf-Funktionen
|
|
* einspritzbar, damit die Suchreihenfolge ohne Installationen testbar ist. */
|
|
export function finden(
|
|
name: string,
|
|
eingestellt = '',
|
|
eigenerOrdner: string = werkzeugOrdner(),
|
|
umgebung: NodeJS.ProcessEnv = process.env,
|
|
existiert: (pfad: string) => boolean = existsSync,
|
|
registry: typeof ausUninstallRegistry = ausUninstallRegistry,
|
|
): string {
|
|
// 1. Ausdrücklich eingestellt — auch wenn der Pfad (noch) fehlt, wird
|
|
// NICHT still etwas anderes genommen: Wer einen Pfad einträgt, will
|
|
// genau den, und ein Tippfehler soll als Fehler sichtbar werden.
|
|
if (eingestellt.trim().length > 0) {
|
|
return existiert(eingestellt.trim()) ? eingestellt.trim() : ''
|
|
}
|
|
// 2 + 4. Eigener Ordner, dann bekannte Orte.
|
|
for (const kandidat of kandidaten(name, eigenerOrdner, umgebung)) {
|
|
if (existiert(kandidat)) return kandidat
|
|
}
|
|
// 3. PATH.
|
|
const imPfad = imPfadSuchen(WERKZEUGE[name].programme, umgebung)
|
|
if (imPfad.length > 0) return imPfad
|
|
// 5. Registry-Rückfall.
|
|
return registry(WERKZEUGE[name].anzeige, WERKZEUGE[name].programme)
|
|
}
|