Files
rippy/rippy-windows/src/kern/werkzeuge/katalog.ts
T
HitonabiandClaude Fable 5 9fa3df500d
Ampel / ampel (push) Successful in 1m22s
feat(v5): Etappe W-6 — Setup, Selbst-Update, Erst-Einrichtung, Lizenzen
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>
2026-08-30 19:03:40 +02:00

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)
}