diff --git a/Scripts/dmg-background.swift b/Scripts/dmg-background.swift new file mode 100644 index 0000000..addde3a --- /dev/null +++ b/Scripts/dmg-background.swift @@ -0,0 +1,125 @@ +// Zeichnet den Hintergrund des Installationsfensters. +// +// Gezeichnet statt mitgeliefert: ein Bild im Verzeichnis wäre eine Datei, die +// niemand mehr anfasst und die bei jeder Farbänderung veraltet. So steht das +// Aussehen im selben Werkzeugkasten wie der Rest — und die Onyx-Farben stehen +// hier genau einmal. +// +// Aufruf: dmg-background +// Erzeugt .png und @2x.png. + +import AppKit + +let arguments = CommandLine.arguments +guard arguments.count == 2 else { + FileHandle.standardError.write(Data("Aufruf: dmg-background \n".utf8)) + exit(1) +} +let target = arguments[1] + +// Maße des Fensters in Punkten. Die Symbole setzt das Skript an dieselben +// Stellen — ändert sich hier etwas, muss es dort mitwandern. +let width: CGFloat = 640 +let height: CGFloat = 400 +let iconY: CGFloat = 196 // Mitte der Symbolreihe, von oben +let leftIcon: CGFloat = 170 +let rightIcon: CGFloat = 470 + +func draw(scale: CGFloat, to path: String) { + let pixelWidth = Int(width * scale) + let pixelHeight = Int(height * scale) + + guard let context = CGContext(data: nil, width: pixelWidth, height: pixelHeight, + bitsPerComponent: 8, bytesPerRow: 0, + space: CGColorSpace(name: CGColorSpace.sRGB)!, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) + else { exit(1) } + + context.scaleBy(x: scale, y: scale) + // Von oben nach unten rechnen wie im Fenster; Core Graphics fängt unten an. + context.translateBy(x: 0, y: height) + context.scaleBy(x: 1, y: -1) + + let graphics = NSGraphicsContext(cgContext: context, flipped: true) + NSGraphicsContext.current = graphics + + // Grundfläche: derselbe flache Verlauf wie im Panel. Zwei Prozent Weiß oben, + // nichts unten — mehr wirkt billig. + let surface = NSColor(srgbRed: 0.043, green: 0.043, blue: 0.051, alpha: 1) + let elevated = NSColor(srgbRed: 0.078, green: 0.078, blue: 0.090, alpha: 1) + NSGradient(colors: [elevated, surface])? + .draw(in: NSRect(x: 0, y: 0, width: width, height: height), angle: -90) + + // Die Haarlinie unter dem Kopf — der halbe „edle" Effekt steckt darin. + NSColor(white: 1, alpha: 0.08).setFill() + NSRect(x: 40, y: 118, width: width - 80, height: 1).fill() + + let title = "Onyx" + let titleAttributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 30, weight: .semibold), + .foregroundColor: NSColor(srgbRed: 0.949, green: 0.949, blue: 0.957, alpha: 1), + .kern: -0.5, + ] + let titleSize = title.size(withAttributes: titleAttributes) + title.draw(at: NSPoint(x: (width - titleSize.width) / 2, y: 48), + withAttributes: titleAttributes) + + let subtitle = "Zum Installieren nach „Programme“ ziehen" + let subtitleAttributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 12, weight: .regular), + .foregroundColor: NSColor(white: 1, alpha: 0.58), + ] + let subtitleSize = subtitle.size(withAttributes: subtitleAttributes) + subtitle.draw(at: NSPoint(x: (width - subtitleSize.width) / 2, y: 88), + withAttributes: subtitleAttributes) + + // Der Pfeil zwischen den beiden Symbolen: er sagt in einem Bild, was der + // Satz darüber in sieben Wörtern sagt. + let accent = NSColor(srgbRed: 0.498, green: 0.659, blue: 1.0, alpha: 0.55) + accent.setStroke() + let shaft = NSBezierPath() + let from = leftIcon + 92 + let to = rightIcon - 92 + shaft.move(to: NSPoint(x: from, y: iconY)) + shaft.line(to: NSPoint(x: to - 10, y: iconY)) + shaft.lineWidth = 3 + shaft.lineCapStyle = .round + shaft.stroke() + + let head = NSBezierPath() + head.move(to: NSPoint(x: to - 18, y: iconY - 9)) + head.line(to: NSPoint(x: to, y: iconY)) + head.line(to: NSPoint(x: to - 18, y: iconY + 9)) + head.lineWidth = 3 + head.lineCapStyle = .round + head.lineJoinStyle = .round + head.stroke() + + // Fußzeile: die Fassung gehört sichtbar dazu, damit man später weiß, was + // man da eigentlich installiert hat. + let version = ProcessInfo.processInfo.environment["ONYX_VERSION"] ?? "" + if !version.isEmpty { + let footer = "Fassung \(version)" + let footerAttributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 10, weight: .regular), + .foregroundColor: NSColor(white: 1, alpha: 0.34), + ] + let footerSize = footer.size(withAttributes: footerAttributes) + footer.draw(at: NSPoint(x: (width - footerSize.width) / 2, y: height - 34), + withAttributes: footerAttributes) + } + + NSGraphicsContext.current = nil + + guard let image = context.makeImage(), + let destination = CGImageDestinationCreateWithURL( + URL(fileURLWithPath: path) as CFURL, "public.png" as CFString, 1, nil) + else { exit(1) } + CGImageDestinationAddImage(destination, image, nil) + CGImageDestinationFinalize(destination) +} + +import ImageIO + +draw(scale: 1, to: target + ".png") +draw(scale: 2, to: target + "@2x.png") diff --git a/Scripts/lib-dmg.sh b/Scripts/lib-dmg.sh new file mode 100644 index 0000000..b70ded5 --- /dev/null +++ b/Scripts/lib-dmg.sh @@ -0,0 +1,75 @@ +# Baut ein Installationsfenster, das aussieht wie eines. +# +# Von beiden Wegen benutzt — dem lokalen wie dem notarisierten —, damit das +# Fenster in beiden Fällen dasselbe ist. +# +# Der Aufwand steckt nicht im Bild, sondern in der Reihenfolge: die +# Fenstergröße, die Symbolplätze und das Hintergrundbild leben in der +# `.DS_Store` des Abbilds. Die schreibt nur der Finder, und der schreibt sie +# nur in ein **beschreibbares** Abbild. Also erst UDRW, dann einrichten, dann +# zusammenpressen. + +# Erwartet: $1 App-Bundle, $2 Ziel-DMG, $3 Fenstertitel, $4 Arbeitsverzeichnis, +# $5 Fassung (für die Fußzeile) +make_pretty_dmg() { + local app="$1" dmg="$2" volname="$3" work="$4" version="$5" + local stage="$work/stage" temp="$work/rw.dmg" + local root; root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + + rm -rf "$stage" "$temp"; mkdir -p "$stage/.background" + + echo "→ Hintergrund zeichnen" + xcrun swiftc -O "$root/Scripts/dmg-background.swift" -o "$work/dmg-bg" 2>/dev/null + ONYX_VERSION="$version" "$work/dmg-bg" "$work/bg" + # Ein TIFF mit beiden Auflösungen: als reines PNG wäre der Hintergrund auf + # einem Retina-Bildschirm doppelt so groß wie das Fenster. + tiffutil -cathidpicheck "$work/bg.png" "$work/bg@2x.png" \ + -out "$stage/.background/background.tiff" >/dev/null + + cp -R "$app" "$stage/" + ln -s /Applications "$stage/Applications" + + echo "→ Abbild anlegen" + hdiutil create -volname "$volname" -srcfolder "$stage" -size 100m \ + -fs HFS+ -format UDRW -ov "$temp" >/dev/null + + local mount="/Volumes/$volname" + hdiutil attach "$temp" -nobrowse -quiet + # Ohne kurze Pause findet das nächste Skript das Volume noch nicht. + sleep 2 + + echo "→ Fenster einrichten" + osascript </dev/null || echo " (Finder nicht erreichbar — das Fenster bleibt schlicht)" +tell application "Finder" + tell disk "$volname" + open + set current view of container window to icon view + set toolbar visible of container window to false + set statusbar visible of container window to false + -- 640 x 400 Punkte Inhalt; die Zahlen sind links, oben, rechts, unten. + set the bounds of container window to {240, 140, 880, 540} + set options to the icon view options of container window + set arrangement of options to not arranged + set icon size of options to 128 + set text size of options to 12 + set background picture of options to file ".background:background.tiff" + set position of item "$(basename "$app")" of container window to {170, 196} + set position of item "Applications" of container window to {470, 196} + update without registering applications + delay 1 + close + end tell +end tell +APPLESCRIPT + + # Der Finder schreibt die .DS_Store verzögert; ohne das Warten liegt im + # fertigen Abbild die alte. + sync + sleep 2 + hdiutil detach "$mount" -quiet || hdiutil detach "$mount" -force -quiet + + echo "→ Zusammenpressen" + rm -f "$dmg" + hdiutil convert "$temp" -format UDZO -imagekey zlib-level=9 -o "$dmg" >/dev/null + rm -f "$temp" +} diff --git a/Scripts/local-dmg.sh b/Scripts/local-dmg.sh index 6ef392b..3634fe8 100755 --- a/Scripts/local-dmg.sh +++ b/Scripts/local-dmg.sh @@ -64,16 +64,8 @@ HELPER_ID=$(codesign -d --verbose=2 "$APP/Contents/MacOS/OnyxHelper" 2>&1 \ echo "✓ Signatur, Entitlements und Helfer-Kennung in Ordnung" step "DMG bauen" -STAGE="$BUILD/stage" -rm -rf "$STAGE"; mkdir -p "$STAGE" -cp -R "$APP" "$STAGE/" -# Der Verweis auf den Programme-Ordner: ohne ihn zieht man die App irgendwohin, -# und der privilegierte Helfer verlangt einen festen Ort. -ln -s /Applications "$STAGE/Applications" - -rm -f "$DMG" -hdiutil create -volname "$APP_NAME $VERSION" -srcfolder "$STAGE" \ - -ov -format UDZO "$DMG" | tail -1 +source "$ROOT/Scripts/lib-dmg.sh" +make_pretty_dmg "$APP" "$DMG" "$APP_NAME" "$BUILD" "$VERSION" echo echo "✓ Fertig: $DMG" diff --git a/Scripts/release.sh b/Scripts/release.sh index e4a8464..5e795ea 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -124,13 +124,8 @@ echo "✓ Signaturen, Entitlements, Helfer-Kennung und Adapter in Ordnung" # ------------------------------------------------------------------- DMG step "DMG bauen" -STAGE="$BUILD/stage" -rm -rf "$STAGE"; mkdir -p "$STAGE" -cp -R "$APP" "$STAGE/" -ln -s /Applications "$STAGE/Applications" - -rm -f "$DMG" -hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DMG" | tail -1 +source "$ROOT/Scripts/lib-dmg.sh" +make_pretty_dmg "$APP" "$DMG" "$APP_NAME" "$BUILD" "$VERSION" # Das DMG selbst wird mitsigniert — sonst meldet Gatekeeper beim Öffnen einen # unbekannten Entwickler, obwohl die App darin einwandfrei ist.