Public Access
The third desktop platform shipped as nothing: a checkout somebody built.
VelopackUpdateChannel already named the two platforms it serves rather than
negating the one it does not, so that a Linux package would be an addition
there rather than something that had silently half-happened. This is it, and
it is deliberately not a Velopack channel.
There is no updater in the package at all. On this platform dnf is already an
updater the user trusts, audits and controls, and a second one answering to
this project instead is the capability ADR 0013 rule 4 spends a page keeping
unsettable. The cost is real and is written down rather than glossed: nothing
tells a Linux user that a newer build exists, because there is no repository
to tell them.
Rule 3 is untouched. The build job packages the release-shaped RPM on every
main build and every tag and uploads nothing, exactly as it does for Windows
and macOS; the desktop job packages the nightly-shaped one and puts it on the
rolling nightly-desktop release beside the Windows feed. Two RPMs with
different names owning different paths cannot replace one another, so rule 9's
separations come almost free here — the profile split still belongs to the
build, through DodoChannel.
vpk verifies that what it packs actually calls VelopackApp.Build().Run(), and
nothing here does that for us, so scripts/ci-linux-rpm.sh makes its own
assertions: it asks the finished package what it requires, what it provides,
what it holds and what modes those carry. Two of those checks exist because
the first realistic payload tripped them.
- .NET's libmscordbi.so links libmscordaccore.so. With bundled provides
excluded — which they must be, or this package would advertise libcoreclr
to the whole system — that produced a dependency no host on earth can
satisfy. The requires exclusion is computed from the payload rather than
listed, because a list of Microsoft's internal file names goes stale in
the direction that yields an uninstallable package.
- cp -a inherits the publish directory's modes, and off the wrong filesystem
that meant a world-writable /opt/DodoSSH: any local account able to
replace the SSH client every other account runs. For an application
holding a team's credentials in plaintext that is the trust model walked
around rather than attacked. Every mode is now set absolutely.
Verified as far as a runner can: both packages install side by side on Fedora
44, resolve webkit2gtk4.1 2.52.5 — the backend MainWindow records as measured
— keep separate launchers, icons and profiles, and dnf remove takes one tree
and leaves the other. 0.0.1~alpha.0.143 sorts below both 0.0.1 and the next
height. What no runner can answer is Phase 19, which is new and unwalked.
RHEL 9 is deliberately not claimed. It ships WebKitGTK's 4.0 line under
libwebkit2gtk-4.0.so.37, and an or-branch for it was written and then removed:
nobody has run the head there, and dnf refusing with "nothing provides
libwebkit2gtk-4.1.so.0" is a better answer than an install that draws a blank
terminal. Same sentence, and the same reason, as the missing Intel Mac.
x86_64 only, for that reason and not a different one.
262 lines
13 KiB
PowerShell
262 lines
13 KiB
PowerShell
# Regenerates dodossh.ico and dodossh.icns from the same geometry the Android launcher icon draws.
|
|
#
|
|
# The phone's mark is a vector — Resources/drawable/ic_launcher_foreground.xml — and the whole
|
|
# reason it is a vector is that there is then one geometry to change and no set of PNG densities
|
|
# to forget one of. Neither desktop platform will take a vector: <ApplicationIcon> wants an .ico
|
|
# and nothing else, Window.Icon wants a bitmap, and vpk wants an .icns for the macOS bundle. So
|
|
# the rasters exist, and this script is how they stay honest: the numbers below are the ones in
|
|
# that XML, and regenerating is the whole edit.
|
|
#
|
|
# pwsh -File src/DodoSSH.Client.App/Assets/dodossh-icon.ps1
|
|
#
|
|
# Both outputs are written every run, deliberately. Two scripts, or one script with a switch,
|
|
# is how the two files come to be drawn from different geometry — which nobody would notice,
|
|
# because no one person looks at a Windows taskbar and a macOS Dock on the same afternoon.
|
|
#
|
|
# Coordinates are the launcher's 108-unit viewport, mapped so the middle 72 fills the canvas.
|
|
# That 72 is not an arbitrary crop: it is the part of an adaptive icon a launcher actually shows,
|
|
# the outer 18 on each edge being what it eats for masking and parallax. Rendering the whole 108
|
|
# here would draw the glyph at half the size it is meant to be — correct arithmetic, and a stamp
|
|
# lost in a field of accent. Matching what the phone displays means matching the 72.
|
|
#
|
|
# The one thing this draws that the phone's vector does not is the tile. On Android the tile is
|
|
# the background layer and the launcher's mask rounds it; Windows has no mask, so the rounding
|
|
# happens here.
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.Drawing
|
|
|
|
$accent = [System.Drawing.ColorTranslator]::FromHtml('#5D42DE') # dodo_accent / AccentColor
|
|
$ink = [System.Drawing.ColorTranslator]::FromHtml('#FFFFFF') # AccentInk
|
|
|
|
# Every size Windows asks for: 16 in a titlebar and a tree, 32 on the desktop, 48 in a large-icon
|
|
# view, 256 for the preview pane. Shipping fewer means Windows downsamples one of these to fill
|
|
# the gap, and its downsampler is not kind to a hairline.
|
|
$sizes = @(16, 20, 24, 32, 40, 48, 64, 128, 256)
|
|
|
|
# $tileFraction is how much of the canvas the accent tile fills, and it is the one number that
|
|
# differs between the two platforms.
|
|
#
|
|
# Windows passes 1.0: the tile bleeds to the edge, because Windows draws application icons at
|
|
# whatever size they come in and every other icon on the taskbar does the same.
|
|
#
|
|
# macOS passes 0.8047, and that is not taste. Apple's icon grid puts a rounded-rect app icon in
|
|
# an 824-pixel square inside a 1024-pixel canvas — 824/1024 — with the remaining hundred pixels a
|
|
# side left as air for the Dock's shadow and its magnification. An icon that ignores the grid and
|
|
# bleeds to the edge does not read as bold; it reads as the one icon in the Dock that is too big,
|
|
# because it sits beside Finder and Safari which do not.
|
|
function New-MarkPng([int]$size, [double]$tileFraction = 1.0)
|
|
{
|
|
$bitmap = New-Object System.Drawing.Bitmap($size, $size, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
$g = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
|
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
|
|
# The tile, and the inset that centres it when it does not fill the canvas.
|
|
$tile = [double]$size * $tileFraction
|
|
$inset = ([double]$size - $tile) / 2.0
|
|
|
|
# The accent tile, rounded as a launcher mask rounds it. A square-cornered tile would be the
|
|
# one icon on the taskbar with corners, which reads as unfinished rather than as deliberate.
|
|
#
|
|
# 0.22 of the tile rather than of the canvas, so the corner keeps its proportion to the shape
|
|
# it is rounding instead of growing as the air around it does. It is also within a whisker of
|
|
# the 185/824 Apple's own grid specifies, which is why one radius serves both files.
|
|
$radius = $tile * 0.22
|
|
$d = $radius * 2.0
|
|
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
|
|
$path.AddArc($inset, $inset, $d, $d, 180, 90)
|
|
$path.AddArc($inset + $tile - $d, $inset, $d, $d, 270, 90)
|
|
$path.AddArc($inset + $tile - $d, $inset + $tile - $d, $d, $d, 0, 90)
|
|
$path.AddArc($inset, $inset + $tile - $d, $d, $d, 90, 90)
|
|
$path.CloseFigure()
|
|
$brush = New-Object System.Drawing.SolidBrush($accent)
|
|
$g.FillPath($brush, $path)
|
|
|
|
# 108-viewport units to pixels, with the outer 18 dropped on each edge. Scaled to the tile and
|
|
# offset by the inset, so the glyph keeps its place within the tile at either fraction.
|
|
$scale = $tile / 72.0
|
|
function P([double]$x, [double]$y) { New-Object System.Drawing.PointF((($x - 18.0) * $scale + $inset), (($y - 18.0) * $scale + $inset)) }
|
|
|
|
# A stroke thinner than a pixel renders as a grey suggestion of itself, which at 16px is the
|
|
# difference between a mark and a smudge. The phone's file already bumps this width for the
|
|
# same reason at 48dp; the floor is that argument carried down to the sizes Windows asks for.
|
|
$pen = New-Object System.Drawing.Pen($ink, [float][Math]::Max(5.4 * $scale, 1.0))
|
|
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
|
|
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
|
|
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
|
|
|
|
# The chevron of >_ and the underscore on the baseline it bottoms out at.
|
|
$g.DrawLines($pen, @((P 36.06 43.33), (P 49.91 53.57), (P 36.06 63.8)))
|
|
$g.DrawLine($pen, (P 54 64.68), (P 71.94 64.68))
|
|
|
|
$stream = New-Object System.IO.MemoryStream
|
|
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
|
|
$pen.Dispose(); $brush.Dispose(); $path.Dispose(); $g.Dispose(); $bitmap.Dispose()
|
|
return $stream.ToArray()
|
|
}
|
|
|
|
# ICO is a six-byte header, a sixteen-byte directory entry per image, then the images. The entries
|
|
# carry PNG payloads rather than the older BMP-with-AND-mask form, which every Windows since Vista
|
|
# reads and which is what keeps a 256px entry from costing 256KB.
|
|
#
|
|
# The cast on each frame is load-bearing. A byte[] returned through PowerShell's output collector
|
|
# comes back as Object[] of boxed bytes, which BinaryWriter has no overload for — it binds to one
|
|
# of the scalar Write()s instead and puts a single byte on the stream. The first run of this
|
|
# script produced a 159-byte .ico that way, header and directory intact and nine one-byte images.
|
|
$frames = New-Object 'System.Collections.Generic.List[byte[]]'
|
|
foreach ($size in $sizes)
|
|
{
|
|
[byte[]]$png = New-MarkPng $size
|
|
$frames.Add($png)
|
|
}
|
|
|
|
$out = New-Object System.IO.MemoryStream
|
|
$w = New-Object System.IO.BinaryWriter($out)
|
|
$w.Write([uint16]0) # reserved
|
|
$w.Write([uint16]1) # type: icon
|
|
$w.Write([uint16]$sizes.Count)
|
|
|
|
$offset = 6 + (16 * $sizes.Count)
|
|
for ($i = 0; $i -lt $sizes.Count; $i++)
|
|
{
|
|
$size = $sizes[$i]
|
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size }))) # 0 means 256
|
|
$w.Write([byte]($(if ($size -ge 256) { 0 } else { $size })))
|
|
$w.Write([byte]0) # palette entries: none, this is truecolour
|
|
$w.Write([byte]0) # reserved
|
|
$w.Write([uint16]1) # colour planes
|
|
$w.Write([uint16]32) # bits per pixel
|
|
$w.Write([uint32]$frames[$i].Length)
|
|
$w.Write([uint32]$offset)
|
|
$offset += $frames[$i].Length
|
|
}
|
|
|
|
foreach ($frame in $frames) { $w.Write($frame) }
|
|
$w.Flush()
|
|
|
|
$target = Join-Path $PSScriptRoot 'dodossh.ico'
|
|
[System.IO.File]::WriteAllBytes($target, $out.ToArray())
|
|
$w.Dispose(); $out.Dispose()
|
|
|
|
Write-Output "Wrote $target ($($sizes.Count) sizes, $((Get-Item $target).Length) bytes)"
|
|
|
|
# ---- dodossh.icns, for the macOS bundle ----------------------------------------------------------
|
|
#
|
|
# Written here rather than by `iconutil` on a Mac, and that is the point of doing it the long way.
|
|
# iconutil is the documented tool and it exists only on macOS, so an icon that needed it could not
|
|
# be regenerated on the machine this project is developed on — the geometry above would change and
|
|
# the .icns would quietly keep the old mark until somebody next opened a Mac. The container format
|
|
# is a magic word, a length and a run of typed PNG chunks, which is little enough to own.
|
|
#
|
|
# ◆ EVERY LENGTH IN THIS FILE IS BIG-ENDIAN, AND BinaryWriter IS NOT.
|
|
#
|
|
# The one thing that will catch anybody editing this. A .icns written little-endian is not rejected
|
|
# with an error — Finder and vpk both just show the placeholder icon, because the first chunk claims
|
|
# a length of about two billion and the parser walks off the end and gives up. Hence Write-BE32.
|
|
#
|
|
# Type codes are Apple's, and the pairs are not redundant. ic08 and ic13 are both 256 pixels because
|
|
# one is "256 at 1x" and the other is "128 at 2x", and a Retina display asked for the second will not
|
|
# accept the first. Same for ic09/ic14 at 512. iconutil emits both from an .iconset for this reason,
|
|
# so this does too.
|
|
$icnsTypes = @(
|
|
@{ Type = 'ic11'; Size = 32 } # 16@2x
|
|
@{ Type = 'ic12'; Size = 64 } # 32@2x
|
|
@{ Type = 'ic07'; Size = 128 } # 128@1x
|
|
@{ Type = 'ic13'; Size = 256 } # 128@2x
|
|
@{ Type = 'ic08'; Size = 256 } # 256@1x
|
|
@{ Type = 'ic14'; Size = 512 } # 256@2x
|
|
@{ Type = 'ic09'; Size = 512 } # 512@1x
|
|
@{ Type = 'ic10'; Size = 1024 } # 512@2x
|
|
)
|
|
|
|
# Apple's icon grid: an 824-pixel shape centred in a 1024-pixel canvas. See New-MarkPng.
|
|
$macTileFraction = 824.0 / 1024.0
|
|
|
|
# Rendered once per distinct pixel size rather than once per type code, so the two 256s and the two
|
|
# 512s are byte-identical and the file does not carry the same image twice over at different
|
|
# compression. It also halves the drawing, which at 1024 is not nothing.
|
|
$rendered = @{}
|
|
foreach ($size in ($icnsTypes.Size | Sort-Object -Unique))
|
|
{
|
|
[byte[]]$png = New-MarkPng $size $macTileFraction
|
|
$rendered[$size] = $png
|
|
}
|
|
|
|
$icns = New-Object System.IO.MemoryStream
|
|
|
|
function Write-BE32([System.IO.Stream]$stream, [uint32]$value)
|
|
{
|
|
$bytes = [System.BitConverter]::GetBytes($value)
|
|
if ([System.BitConverter]::IsLittleEndian) { [array]::Reverse($bytes) }
|
|
$stream.Write($bytes, 0, 4)
|
|
}
|
|
|
|
function Write-Ascii([System.IO.Stream]$stream, [string]$text)
|
|
{
|
|
$bytes = [System.Text.Encoding]::ASCII.GetBytes($text)
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
}
|
|
|
|
# The header's length field covers the whole file including the header, so it is written last —
|
|
# eight bytes of nothing now, seeked back to and filled in once the total is known.
|
|
Write-Ascii $icns 'icns'
|
|
Write-BE32 $icns 0
|
|
|
|
foreach ($entry in $icnsTypes)
|
|
{
|
|
$payload = $rendered[$entry.Size]
|
|
Write-Ascii $icns $entry.Type
|
|
|
|
# Length includes this chunk's own eight-byte header, which is the off-by-eight everybody
|
|
# writes once.
|
|
Write-BE32 $icns ([uint32]($payload.Length + 8))
|
|
$icns.Write($payload, 0, $payload.Length)
|
|
}
|
|
|
|
$total = [uint32]$icns.Length
|
|
$icns.Position = 4
|
|
Write-BE32 $icns $total
|
|
|
|
$icnsTarget = Join-Path $PSScriptRoot 'dodossh.icns'
|
|
[System.IO.File]::WriteAllBytes($icnsTarget, $icns.ToArray())
|
|
$icns.Dispose()
|
|
|
|
Write-Output "Wrote $icnsTarget ($($icnsTypes.Count) entries, $((Get-Item $icnsTarget).Length) bytes)"
|
|
|
|
# ---- dodossh-<size>.png, for the Linux desktop entry ---------------------------------------------
|
|
#
|
|
# The third platform, and the only one that wants the raster loose rather than in a container. A
|
|
# freedesktop icon theme is a directory tree — hicolor/48x48/apps/dodossh.png and friends — and the
|
|
# RPM built by scripts/ci-linux-rpm.sh installs one file per size into it.
|
|
#
|
|
# Written here for the reason this whole file exists: one geometry, regenerated in one command. The
|
|
# alternative was to have the packaging script pull the 256px frame out of dodossh.ico at build time,
|
|
# which works — the .ico's entries really are PNGs — and puts a second place that has to know how an
|
|
# ICO directory is laid out. It also silently ties the Linux icon to whatever sizes Windows happens
|
|
# to want.
|
|
#
|
|
# ◆ THE TILE BLEEDS TO THE EDGE HERE, AS IT DOES ON WINDOWS.
|
|
#
|
|
# tileFraction stays 1.0. The 0.8047 inset above is Apple's icon grid and nothing else — GNOME, KDE
|
|
# and every panel that reads hicolor draw the icon at the size they asked for, with no shadow and no
|
|
# magnification to leave air for. An inset icon would simply be the small one in the launcher.
|
|
#
|
|
# ◆ AND THE SIZES ARE NOT A ROUND NUMBER OF THEM.
|
|
#
|
|
# 48 is the size a GNOME dash and most panels ask for, 128 and 256 are what a large-icon file manager
|
|
# and GNOME Software use, and 64 is between them because the 48 upscaled to it is visibly soft. There
|
|
# is deliberately nothing below 48: the hairline floor in New-MarkPng keeps a 16px mark legible, but
|
|
# no Linux desktop asks this theme for one — window titlebars there draw no icon at all.
|
|
$linuxSizes = @(48, 64, 128, 256)
|
|
|
|
foreach ($size in $linuxSizes)
|
|
{
|
|
[byte[]]$png = New-MarkPng $size
|
|
$pngTarget = Join-Path $PSScriptRoot "dodossh-$size.png"
|
|
[System.IO.File]::WriteAllBytes($pngTarget, $png)
|
|
Write-Output "Wrote $pngTarget ($((Get-Item $pngTarget).Length) bytes)"
|
|
}
|