mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-17 16:10:09 +01:00
feat(setup): add real-time Docker pull progress via Docker Engine API
- Use Docker Engine API for accurate download progress with byte counts - Add SSE streaming for smooth real-time UI updates (replaces polling) - Show toast notification when Docker completes in background mode - Fix progress calculation to use bytes instead of layer count - Fix framework selection border clipping with padding - Pull pre-built image from ghcr.io/wailsapp/wails-cross instead of building locally
This commit is contained in:
parent
ecec018d0c
commit
f763cf3e2f
16 changed files with 2377 additions and 487 deletions
BIN
v3/internal/setupwizard/assets/apple-sdk-license.pdf
Normal file
BIN
v3/internal/setupwizard/assets/apple-sdk-license.pdf
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
v3/internal/setupwizard/frontend/dist/assets/index-CGNSfeb6.css
vendored
Normal file
1
v3/internal/setupwizard/frontend/dist/assets/index-CGNSfeb6.css
vendored
Normal file
File diff suppressed because one or more lines are too long
60
v3/internal/setupwizard/frontend/dist/assets/index-Ced0vwAS.js
vendored
Normal file
60
v3/internal/setupwizard/frontend/dist/assets/index-Ced0vwAS.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,8 +7,8 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<script type="module" crossorigin src="/assets/index-CEPUVERO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BKINrEPO.css">
|
||||
<script type="module" crossorigin src="/assets/index-Ced0vwAS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CGNSfeb6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
319
v3/internal/setupwizard/frontend/mockup.html
Normal file
319
v3/internal/setupwizard/frontend/mockup.html
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Wails – Project Defaults (Mock)</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg0:#05070b;
|
||||
--bg1:#070a11;
|
||||
--panel:#0b0f18;
|
||||
--panel2:#0a0e16;
|
||||
--text: rgba(255,255,255,.92);
|
||||
--text2: rgba(255,255,255,.62);
|
||||
--line: rgba(255,255,255,.08);
|
||||
--hover: rgba(255,255,255,.06);
|
||||
--shadow: rgba(0,0,0,.55);
|
||||
--accent:#e0242a;
|
||||
}
|
||||
|
||||
*{ box-sizing:border-box; }
|
||||
html,body{ height:100%; }
|
||||
body{
|
||||
margin:0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
||||
background:
|
||||
radial-gradient(900px 600px at 60% 40%, rgba(255,0,40,.08), transparent 55%),
|
||||
radial-gradient(1100px 900px at 65% 55%, rgba(255,255,255,.06), transparent 60%),
|
||||
radial-gradient(900px 600px at 30% 60%, rgba(255,255,255,.04), transparent 60%),
|
||||
linear-gradient(180deg, var(--bg0), var(--bg1));
|
||||
color:var(--text);
|
||||
display:grid;
|
||||
place-items:center;
|
||||
}
|
||||
|
||||
/* Window */
|
||||
.window{
|
||||
width:min(1160px, 96vw);
|
||||
height:min(720px, 92vh);
|
||||
border-radius:18px;
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
|
||||
box-shadow:
|
||||
0 30px 80px rgba(0,0,0,.65),
|
||||
0 0 0 1px rgba(255,255,255,.06) inset;
|
||||
}
|
||||
|
||||
.layout{
|
||||
height:100%;
|
||||
display:grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar{
|
||||
position:relative;
|
||||
background:
|
||||
radial-gradient(1000px 600px at 40% 20%, rgba(255,0,50,.10), transparent 55%),
|
||||
linear-gradient(180deg, rgba(5,7,12,.92), rgba(5,7,12,.74));
|
||||
box-shadow: 1px 0 0 rgba(255,255,255,.06) inset;
|
||||
padding:28px 22px 18px;
|
||||
}
|
||||
|
||||
.brand{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:flex-start;
|
||||
gap:10px;
|
||||
padding-bottom:18px;
|
||||
}
|
||||
|
||||
/* Placeholder "dragon" */
|
||||
.logo{
|
||||
width:90px; height:64px;
|
||||
position:relative;
|
||||
filter: drop-shadow(0 10px 18px rgba(224,36,42,.35));
|
||||
}
|
||||
.logo:before, .logo:after{
|
||||
content:"";
|
||||
position:absolute;
|
||||
inset:0;
|
||||
background: conic-gradient(from 210deg, #ff4b4f, #b90f15, #ff4b4f);
|
||||
clip-path: polygon(8% 65%, 40% 20%, 58% 30%, 78% 8%, 84% 30%, 64% 44%, 92% 52%, 70% 66%, 78% 88%, 55% 74%, 42% 92%, 34% 66%, 10% 82%);
|
||||
border-radius:10px;
|
||||
}
|
||||
.logo:after{
|
||||
inset:6px;
|
||||
opacity:.55;
|
||||
filter: blur(.2px);
|
||||
}
|
||||
|
||||
.wordmark{
|
||||
letter-spacing:.46em;
|
||||
font-weight:700;
|
||||
font-size:14px;
|
||||
color: rgba(255,255,255,.88);
|
||||
margin-left:4px;
|
||||
}
|
||||
|
||||
.nav{
|
||||
margin-top:8px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:10px;
|
||||
}
|
||||
.nav a{
|
||||
text-decoration:none;
|
||||
color: rgba(255,255,255,.62);
|
||||
font-size:18px;
|
||||
padding:14px 14px;
|
||||
border-radius:12px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:12px;
|
||||
transition: background-color .12s ease, color .12s ease;
|
||||
}
|
||||
.nav a.active{
|
||||
background: rgba(255,255,255,.08);
|
||||
color: rgba(255,255,255,.92);
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.06) inset;
|
||||
}
|
||||
.dot{
|
||||
width:12px; height:12px; border-radius:999px;
|
||||
background: rgba(255,255,255,.15);
|
||||
flex:0 0 auto;
|
||||
}
|
||||
|
||||
/* Main panel */
|
||||
.main{
|
||||
position:relative;
|
||||
padding:48px 56px;
|
||||
background:
|
||||
radial-gradient(1200px 900px at 55% 40%, rgba(255,255,255,.05), transparent 60%),
|
||||
linear-gradient(180deg, rgba(5,7,12,.40), rgba(5,7,12,.20));
|
||||
}
|
||||
|
||||
.content{
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
text-align:center;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
h1{
|
||||
margin:0;
|
||||
font-size: 38px;
|
||||
line-height: 44px;
|
||||
letter-spacing: .01em;
|
||||
font-weight: 650;
|
||||
color: rgba(255,255,255,.92);
|
||||
}
|
||||
|
||||
.sub{
|
||||
margin:10px 0 26px 0;
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
color: rgba(255,255,255,.62);
|
||||
}
|
||||
|
||||
/* Preference group */
|
||||
.group{
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
background: rgba(255,255,255,.12);
|
||||
box-shadow:
|
||||
0 12px 30px rgba(0,0,0,.35),
|
||||
0 0 0 1px rgba(255,255,255,.06) inset;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
overflow:hidden;
|
||||
text-align:left;
|
||||
}
|
||||
|
||||
.row{
|
||||
height: 52px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding: 0 20px;
|
||||
cursor:pointer;
|
||||
user-select:none;
|
||||
transition: background-color .12s ease;
|
||||
}
|
||||
.row:hover{ background: var(--hover); }
|
||||
.row + .row{ border-top: 1px solid var(--line); }
|
||||
|
||||
.label{
|
||||
font-size: 13px;
|
||||
font-weight: 550;
|
||||
color: rgba(255,255,255,.90);
|
||||
}
|
||||
|
||||
.value{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: rgba(255,255,255,.65);
|
||||
max-width: 360px;
|
||||
}
|
||||
.value span{
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
text-align:right;
|
||||
display:block;
|
||||
max-width: 330px;
|
||||
}
|
||||
.chev{
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,.40);
|
||||
transition: color .12s ease;
|
||||
transform: translateY(-.5px);
|
||||
}
|
||||
.row:hover .chev{ color: rgba(255,255,255,.65); }
|
||||
|
||||
/* Continue button */
|
||||
.actions{
|
||||
margin-top: 86px;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
}
|
||||
button{
|
||||
height: 44px;
|
||||
padding: 0 34px;
|
||||
border-radius: 12px;
|
||||
border: 0;
|
||||
background: linear-gradient(180deg, rgba(224,36,42,.95), rgba(184,16,22,.95));
|
||||
color: rgba(255,255,255,.92);
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
letter-spacing: .01em;
|
||||
box-shadow:
|
||||
0 14px 30px rgba(224,36,42,.22),
|
||||
0 0 0 1px rgba(255,255,255,.12) inset;
|
||||
cursor:pointer;
|
||||
transition: transform .08s ease, filter .12s ease;
|
||||
}
|
||||
button:active{ transform: translateY(1px); filter: brightness(.98); }
|
||||
|
||||
/* Optional: subtle film grain */
|
||||
.grain{
|
||||
pointer-events:none;
|
||||
position:absolute;
|
||||
inset:0;
|
||||
opacity:.10;
|
||||
mix-blend-mode: overlay;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
|
||||
background-size: 220px 220px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window" role="application" aria-label="Wails onboarding wizard">
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="logo" aria-hidden="true"></div>
|
||||
<div class="wordmark">WAILS</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav" aria-label="Setup steps">
|
||||
<a href="#" aria-current="false"><span class="dot"></span>Welcome</a>
|
||||
<a href="#" aria-current="false"><span class="dot"></span>Dependencies</a>
|
||||
<a href="#" aria-current="false"><span class="dot"></span>Platform</a>
|
||||
<a class="active" href="#" aria-current="page"><span class="dot"></span>Defaults</a>
|
||||
<a href="#" aria-current="false"><span class="dot"></span>Complete</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div class="content">
|
||||
<h1>Project Defaults</h1>
|
||||
<div class="sub">Choose how new Wails apps are created by default.</div>
|
||||
|
||||
<section class="group" aria-label="Project defaults">
|
||||
<div class="row" tabindex="0" role="button" aria-label="Edit bundle identifier">
|
||||
<div class="label">Bundle identifier</div>
|
||||
<div class="value"><span>com.example</span><div class="chev">▸</div></div>
|
||||
</div>
|
||||
<div class="row" tabindex="0" role="button" aria-label="Edit version">
|
||||
<div class="label">Version</div>
|
||||
<div class="value"><span>0.1.0</span><div class="chev">▸</div></div>
|
||||
</div>
|
||||
<div class="row" tabindex="0" role="button" aria-label="Choose template">
|
||||
<div class="label">Template</div>
|
||||
<div class="value"><span>Vanilla (JavaScript)</span><div class="chev">▸</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="grain"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tiny affordance: allow Enter/Space to activate rows (no actual editing yet).
|
||||
document.querySelectorAll(".row").forEach(row => {
|
||||
row.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
row.click();
|
||||
}
|
||||
});
|
||||
row.addEventListener("click", () => {
|
||||
// Stub: replace with your routing / inline edit (Step 3).
|
||||
console.log("Row activated:", row.querySelector(".label").textContent.trim());
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,40 @@ export async function getDockerStatus(): Promise<DockerStatus> {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
export function subscribeDockerStatus(onUpdate: (status: DockerStatus) => void): () => void {
|
||||
let eventSource: EventSource | null = null;
|
||||
let closed = false;
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
|
||||
eventSource = new EventSource(`${API_BASE}/docker/status/stream`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const status = JSON.parse(event.data) as DockerStatus;
|
||||
onUpdate(status);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse docker status:', e);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource?.close();
|
||||
if (!closed) {
|
||||
setTimeout(connect, 1000);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
closed = true;
|
||||
eventSource?.close();
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildDockerImage(): Promise<{ status: string }> {
|
||||
const response = await fetch(`${API_BASE}/docker/build`, { method: 'POST' });
|
||||
return response.json();
|
||||
|
|
|
|||
|
|
@ -187,7 +187,6 @@ html, body, #root {
|
|||
animation: scrollBackground 60s linear infinite;
|
||||
}
|
||||
|
||||
/* Reduced motion support - but preserve loading spinners */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*:not(.animate-spin):not(.spinner), *:not(.animate-spin):not(.spinner)::before, *:not(.animate-spin):not(.spinner)::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
|
|
@ -195,3 +194,20 @@ html, body, #root {
|
|||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.settings-row:focus-visible {
|
||||
outline: 2px solid #ef4444;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@ export interface DockerStatus {
|
|||
imageName: string;
|
||||
imageSize?: string;
|
||||
pullProgress: number;
|
||||
pullMessage?: string;
|
||||
pullStatus: 'idle' | 'pulling' | 'complete' | 'error';
|
||||
pullError?: string;
|
||||
bytesTotal?: number;
|
||||
bytesDone?: number;
|
||||
layerCount?: number;
|
||||
layersDone?: number;
|
||||
}
|
||||
|
||||
export interface UserConfig {
|
||||
|
|
|
|||
212
v3/internal/setupwizard/pull_parser_test.go
Normal file
212
v3/internal/setupwizard/pull_parser_test.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package setupwizard
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected float64
|
||||
}{
|
||||
{"10B", 10},
|
||||
{"10KB", 10 * 1024},
|
||||
{"10MB", 10 * 1024 * 1024},
|
||||
{"10GB", 10 * 1024 * 1024 * 1024},
|
||||
{"1.5MB", 1.5 * 1024 * 1024},
|
||||
{"100", 100},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := parseSize(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("parseSize(%q) = %v, want %v", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullParser(t *testing.T) {
|
||||
dockerOutput := `latest: Pulling from wailsapp/wails-cross
|
||||
1074353eec0d: Already exists
|
||||
665f62578bce: Already exists
|
||||
61d7d5b4f231: Pulling fs layer
|
||||
202b93a508bb: Pulling fs layer
|
||||
61d7d5b4f231: Downloading 1.5MB/10MB
|
||||
202b93a508bb: Downloading 2MB/20MB
|
||||
61d7d5b4f231: Downloading 5MB/10MB
|
||||
202b93a508bb: Downloading 10MB/20MB
|
||||
61d7d5b4f231: Verifying Checksum
|
||||
61d7d5b4f231: Download complete
|
||||
61d7d5b4f231: Extracting 1MB/10MB
|
||||
61d7d5b4f231: Extracting 10MB/10MB
|
||||
61d7d5b4f231: Pull complete
|
||||
202b93a508bb: Verifying Checksum
|
||||
202b93a508bb: Download complete
|
||||
202b93a508bb: Extracting 5MB/20MB
|
||||
202b93a508bb: Extracting 20MB/20MB
|
||||
202b93a508bb: Pull complete
|
||||
Digest: sha256:abc123
|
||||
Status: Downloaded newer image`
|
||||
|
||||
parser := newPullParser()
|
||||
lines := strings.Split(dockerOutput, "\n")
|
||||
|
||||
type checkpoint struct {
|
||||
lineContains string
|
||||
minProgress int
|
||||
maxProgress int
|
||||
stage string
|
||||
}
|
||||
|
||||
checkpoints := []checkpoint{
|
||||
{"Pulling from", 0, 0, "Connecting"},
|
||||
{"Pulling fs layer", 0, 0, "Downloading"},
|
||||
{"Downloading 1.5MB/10MB", 1, 20, "Downloading"},
|
||||
{"Downloading 5MB/10MB", 20, 60, "Downloading"},
|
||||
{"Download complete", 30, 70, "Extracting"},
|
||||
{"Pull complete", 30, 100, "Extracting"},
|
||||
}
|
||||
|
||||
checkIdx := 0
|
||||
for _, line := range lines {
|
||||
progress := parser.ParseLine(line)
|
||||
|
||||
if checkIdx < len(checkpoints) && strings.Contains(line, checkpoints[checkIdx].lineContains) {
|
||||
cp := checkpoints[checkIdx]
|
||||
if progress.Progress < cp.minProgress || progress.Progress > cp.maxProgress {
|
||||
t.Errorf("After %q: progress=%d, want between %d and %d",
|
||||
cp.lineContains, progress.Progress, cp.minProgress, cp.maxProgress)
|
||||
}
|
||||
if progress.Stage != cp.stage {
|
||||
t.Errorf("After %q: stage=%q, want %q", cp.lineContains, progress.Stage, cp.stage)
|
||||
}
|
||||
checkIdx++
|
||||
}
|
||||
}
|
||||
|
||||
finalProgress := parser.ParseLine("")
|
||||
if finalProgress.Progress != 100 {
|
||||
t.Errorf("Final progress = %d, want 100", finalProgress.Progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullParserRealOutput(t *testing.T) {
|
||||
realOutput := `latest: Pulling from wailsapp/wails-cross
|
||||
1074353eec0d: Already exists
|
||||
665f62578bce: Already exists
|
||||
61d7d5b4f231: Pulling fs layer
|
||||
202b93a508bb: Pulling fs layer
|
||||
604349a0d76e: Pulling fs layer
|
||||
61d7d5b4f231: Downloading 1MB/10MB
|
||||
202b93a508bb: Downloading 2MB/20MB
|
||||
604349a0d76e: Downloading 1MB/15MB
|
||||
61d7d5b4f231: Downloading 5MB/10MB
|
||||
202b93a508bb: Downloading 10MB/20MB
|
||||
604349a0d76e: Downloading 8MB/15MB
|
||||
61d7d5b4f231: Verifying Checksum
|
||||
61d7d5b4f231: Download complete
|
||||
61d7d5b4f231: Pull complete
|
||||
202b93a508bb: Verifying Checksum
|
||||
202b93a508bb: Download complete
|
||||
202b93a508bb: Pull complete
|
||||
604349a0d76e: Verifying Checksum
|
||||
604349a0d76e: Download complete
|
||||
604349a0d76e: Pull complete
|
||||
Digest: sha256:abc123
|
||||
Status: Downloaded newer image`
|
||||
|
||||
parser := newPullParser()
|
||||
lines := strings.Split(realOutput, "\n")
|
||||
|
||||
var lastProgress PullProgress
|
||||
for _, line := range lines {
|
||||
lastProgress = parser.ParseLine(line)
|
||||
t.Logf("Line: %q -> Stage: %s, Progress: %d%%", line, lastProgress.Stage, lastProgress.Progress)
|
||||
}
|
||||
|
||||
if lastProgress.Stage == "Connecting" {
|
||||
t.Errorf("Stage should not be 'Connecting' after parsing layers, got: %s", lastProgress.Stage)
|
||||
}
|
||||
if lastProgress.Progress != 100 {
|
||||
t.Errorf("Final progress should be 100%%, got: %d%%", lastProgress.Progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullParserWithANSI(t *testing.T) {
|
||||
lines := []string{
|
||||
"latest: Pulling from wailsapp/wails-cross",
|
||||
"[1A[2K1074353eec0d: Pulling fs layer [1B",
|
||||
"[1A[2K665f62578bce: Pulling fs layer [1B",
|
||||
"[22A[2K665f62578bce: Downloading [==> ] 16.38kB/296.1kB[22B",
|
||||
"[21A[2K5c445a0e108b: Downloading [> ] 538.1kB/60.15MB[21B",
|
||||
"[23A[2K1074353eec0d: Downloading [=================================================> ] 3.811MB/3.86MB[23B",
|
||||
"[23A[2K1074353eec0d: Verifying Checksum [23B",
|
||||
"[23A[2K1074353eec0d: Download complete [23B",
|
||||
"[23A[2K1074353eec0d: Extracting [> ] 65.54kB/3.86MB[23B",
|
||||
"[23A[2K1074353eec0d: Pull complete [23B",
|
||||
"[22A[2K665f62578bce: Pull complete [22B",
|
||||
}
|
||||
|
||||
parser := newPullParser()
|
||||
var lastProgress PullProgress
|
||||
for _, line := range lines {
|
||||
lastProgress = parser.ParseLine(line)
|
||||
t.Logf("Line: %.60q -> Stage: %s, Progress: %d%%", line, lastProgress.Stage, lastProgress.Progress)
|
||||
}
|
||||
|
||||
if lastProgress.Stage == "Connecting" {
|
||||
t.Errorf("Stage should not be 'Connecting', got: %s", lastProgress.Stage)
|
||||
}
|
||||
|
||||
if len(parser.layerSizes) == 0 {
|
||||
t.Errorf("Should have parsed layer sizes, got none")
|
||||
}
|
||||
|
||||
if lastProgress.Progress == 0 {
|
||||
t.Errorf("Progress should not be 0 after Pull complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullParserNoSizeInfo(t *testing.T) {
|
||||
output := `latest: Pulling from wailsapp/wails-cross
|
||||
layer1: Pulling fs layer
|
||||
layer2: Pulling fs layer
|
||||
layer3: Pulling fs layer
|
||||
layer4: Pulling fs layer
|
||||
layer1: Verifying Checksum
|
||||
layer1: Download complete
|
||||
layer1: Pull complete
|
||||
layer2: Verifying Checksum
|
||||
layer2: Download complete
|
||||
layer2: Pull complete
|
||||
layer3: Verifying Checksum
|
||||
layer3: Download complete
|
||||
layer3: Pull complete
|
||||
layer4: Verifying Checksum
|
||||
layer4: Download complete
|
||||
layer4: Pull complete`
|
||||
|
||||
parser := newPullParser()
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
progressHistory := []int{}
|
||||
for _, line := range lines {
|
||||
progress := parser.ParseLine(line)
|
||||
progressHistory = append(progressHistory, progress.Progress)
|
||||
t.Logf("Line: %q -> Stage: %s, Progress: %d%%", line, progress.Stage, progress.Progress)
|
||||
}
|
||||
|
||||
finalProgress := progressHistory[len(progressHistory)-1]
|
||||
if finalProgress != 100 {
|
||||
t.Errorf("Final progress should be 100%%, got: %d%%", finalProgress)
|
||||
}
|
||||
|
||||
for i := 1; i < len(progressHistory); i++ {
|
||||
if progressHistory[i] < progressHistory[i-1] {
|
||||
t.Errorf("Progress should not decrease: %d -> %d", progressHistory[i-1], progressHistory[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,9 @@ import (
|
|||
func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
||||
var deps []DependencyStatus
|
||||
|
||||
// Check Go (required)
|
||||
deps = append(deps, checkGo())
|
||||
|
||||
// Check Xcode Command Line Tools
|
||||
deps = append(deps, checkXcode())
|
||||
|
||||
|
|
@ -23,6 +26,44 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
|||
return deps
|
||||
}
|
||||
|
||||
func checkGo() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "Go",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommand("go", "version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Go 1.25+ is required"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
||||
parts := strings.Split(version, " ")
|
||||
if len(parts) >= 3 {
|
||||
versionStr := strings.TrimPrefix(parts[2], "go")
|
||||
dep.Version = versionStr
|
||||
|
||||
versionParts := strings.Split(versionStr, ".")
|
||||
if len(versionParts) >= 2 {
|
||||
major, _ := strconv.Atoi(versionParts[0])
|
||||
minor, _ := strconv.Atoi(versionParts[1])
|
||||
if major < 1 || (major == 1 && minor < 25) {
|
||||
dep.Status = "needs_update"
|
||||
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkXcode() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "Xcode Command Line Tools",
|
||||
|
|
|
|||
|
|
@ -14,13 +14,12 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
|||
var deps []DependencyStatus
|
||||
hasNpm := false
|
||||
|
||||
// Get OS info for package manager detection
|
||||
deps = append(deps, checkGo())
|
||||
|
||||
info, _ := operatingsystem.Info()
|
||||
|
||||
// Find the package manager
|
||||
pm := packagemanager.Find(info.ID)
|
||||
if pm != nil {
|
||||
// Get platform dependencies from the doctor package
|
||||
platformDeps, _ := packagemanager.Dependencies(pm)
|
||||
for _, dep := range platformDeps {
|
||||
if dep.Name == "npm" {
|
||||
|
|
@ -45,17 +44,54 @@ func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
|||
}
|
||||
}
|
||||
|
||||
// Check npm (common dependency) - only if not already added by package manager
|
||||
if !hasNpm {
|
||||
deps = append(deps, checkNpm())
|
||||
}
|
||||
|
||||
// Check Docker (optional)
|
||||
deps = append(deps, checkDocker())
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
func checkGo() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "Go",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommand("go", "version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Go 1.25+ is required"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
dep.InstallCommand = "Download from https://go.dev/dl/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
||||
parts := strings.Split(version, " ")
|
||||
if len(parts) >= 3 {
|
||||
versionStr := strings.TrimPrefix(parts[2], "go")
|
||||
dep.Version = versionStr
|
||||
|
||||
versionParts := strings.Split(versionStr, ".")
|
||||
if len(versionParts) >= 2 {
|
||||
major, _ := strconv.Atoi(versionParts[0])
|
||||
minor, _ := strconv.Atoi(versionParts[1])
|
||||
if major < 1 || (major == 1 && minor < 25) {
|
||||
dep.Status = "needs_update"
|
||||
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkNpm() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "npm",
|
||||
|
|
|
|||
|
|
@ -60,18 +60,52 @@ func execCommandRefreshed(name string, args ...string) (string, error) {
|
|||
func (w *Wizard) checkAllDependencies() []DependencyStatus {
|
||||
var deps []DependencyStatus
|
||||
|
||||
// Check WebView2 Runtime
|
||||
deps = append(deps, checkGo())
|
||||
deps = append(deps, checkWebView2())
|
||||
|
||||
// Check npm (common dependency)
|
||||
deps = append(deps, checkNpm())
|
||||
|
||||
// Check Docker (optional)
|
||||
deps = append(deps, checkDocker())
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
func checkGo() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "Go",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
version, err := execCommandRefreshed("go", "version")
|
||||
if err != nil {
|
||||
dep.Status = "not_installed"
|
||||
dep.Installed = false
|
||||
dep.Message = "Go 1.25+ is required"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
return dep
|
||||
}
|
||||
|
||||
dep.Installed = true
|
||||
dep.Status = "installed"
|
||||
|
||||
parts := strings.Split(version, " ")
|
||||
if len(parts) >= 3 {
|
||||
versionStr := strings.TrimPrefix(parts[2], "go")
|
||||
dep.Version = versionStr
|
||||
|
||||
versionParts := strings.Split(versionStr, ".")
|
||||
if len(versionParts) >= 2 {
|
||||
major, _ := strconv.Atoi(versionParts[0])
|
||||
minor, _ := strconv.Atoi(versionParts[1])
|
||||
if major < 1 || (major == 1 && minor < 25) {
|
||||
dep.Status = "needs_update"
|
||||
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"
|
||||
dep.HelpURL = "https://go.dev/dl/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func checkWebView2() DependencyStatus {
|
||||
dep := DependencyStatus{
|
||||
Name: "WebView2 Runtime",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue