feat(render): implement PowerShell installer rendering

Add PowerShell() function to render .ps1 installers by injecting
$Env: variables and splicing install.ps1 content. Wire it into
the webid server for .ps1 extension requests.
This commit is contained in:
AJ ONeal
2026-03-11 12:05:15 -06:00
parent a76413012f
commit 9095b34c22
3 changed files with 107 additions and 11 deletions

View File

@@ -75,6 +75,33 @@ func TestInstallerFull(t *testing.T) {
t.Logf("installer size: %d bytes", len(body))
}
// TestInstallerPowerShell verifies /api/installers/{pkg}.ps1 returns a PowerShell installer.
func TestInstallerPowerShell(t *testing.T) {
srv, ts := newTestServer(t)
pkg := "node"
if srv.getPackage(pkg) == nil {
t.Skipf("package %s not in cache", pkg)
}
code, body := getWithUA(t, ts, "/api/installers/node@stable.ps1", "AMD64/unknown Windows/10.0.19045 msvc")
if code != 200 {
t.Fatalf("status %d: %s", code, body[:min(len(body), 500)])
}
if !strings.Contains(body, "$Env:WEBI_VERSION") {
t.Error("missing $Env:WEBI_VERSION in PS1 installer")
}
if !strings.Contains(body, "$Env:WEBI_PKG_URL") {
t.Error("missing $Env:WEBI_PKG_URL in PS1 installer")
}
if !strings.Contains(body, "$Env:PKG_NAME") {
t.Error("missing $Env:PKG_NAME in PS1 installer")
}
t.Logf("PS1 installer size: %d bytes", len(body))
}
// TestInstallerSelfHosted verifies selfhosted packages get a script without resolution.
func TestInstallerSelfHosted(t *testing.T) {
_, ts := newTestServer(t)

View File

@@ -668,12 +668,6 @@ func (s *server) handleInstaller(w http.ResponseWriter, r *http.Request) {
http.Error(w, "package name required", http.StatusBadRequest)
return
}
if ext == "ps1" {
// TODO: PowerShell installer rendering.
http.Error(w, "PowerShell installer not yet implemented", http.StatusNotImplemented)
return
}
// Detect platform from User-Agent.
ua := uadetect.FromRequest(r)
if ua.OS == "" {
@@ -763,11 +757,18 @@ func (s *server) handleInstaller(w http.ResponseWriter, r *http.Request) {
p.ReleasesURL = fmt.Sprintf("%s/api/releases/%s@%s.tab?os=%s&arch=%s&libc=%s&formats=tar&pretty=true",
baseURL, pkg, tag, p.OS, p.Arch, p.Libc)
tplPath := filepath.Join(s.installersDir, "_webi", "package-install.tpl.sh")
script, err := render.Bash(tplPath, s.installersDir, pkg, p)
if err != nil {
log.Printf("render %s: %v", pkg, err)
http.Error(w, fmt.Sprintf("failed to render installer for %q: %v", pkg, err), http.StatusInternalServerError)
var script string
var renderErr error
if ext == "ps1" {
tplPath := filepath.Join(s.installersDir, "_webi", "package-install.tpl.ps1")
script, renderErr = render.PowerShell(tplPath, s.installersDir, pkg, p)
} else {
tplPath := filepath.Join(s.installersDir, "_webi", "package-install.tpl.sh")
script, renderErr = render.Bash(tplPath, s.installersDir, pkg, p)
}
if renderErr != nil {
log.Printf("render %s: %v", pkg, renderErr)
http.Error(w, fmt.Sprintf("failed to render installer for %q: %v", pkg, renderErr), http.StatusInternalServerError)
return
}

View File

@@ -133,6 +133,74 @@ func Bash(tplPath, installersDir, pkgName string, p Params) (string, error) {
return text, nil
}
// PowerShell renders a complete PowerShell installer script by injecting
// params into the template and splicing in the package's install.ps1.
func PowerShell(tplPath, installersDir, pkgName string, p Params) (string, error) {
tpl, err := os.ReadFile(tplPath)
if err != nil {
return "", fmt.Errorf("render: read template: %w", err)
}
installPath := filepath.Join(installersDir, pkgName, "install.ps1")
installPs1, err := os.ReadFile(installPath)
if err != nil {
return "", fmt.Errorf("render: read %s/install.ps1: %w", pkgName, err)
}
text := string(tpl)
vars := []struct {
name string
value string
}{
{"WEBI_PKG", p.PkgName + "@" + p.Tag},
{"WEBI_HOST", p.Host},
{"WEBI_VERSION", p.Version},
{"WEBI_GIT_TAG", p.GitTag},
{"WEBI_PKG_URL", p.PkgURL},
{"WEBI_PKG_FILE", p.PkgFile},
{"WEBI_PKG_PATHNAME", p.PkgFile},
{"PKG_NAME", p.PkgName},
}
for _, v := range vars {
text = InjectPSVar(text, v.name, v.value)
}
text = strings.Replace(text, "# {{ installer }}", string(installPs1), 1)
text = strings.Replace(text, "{{ installer }}", string(installPs1), 1)
return text, nil
}
// InjectPSVar replaces a PowerShell template variable line with its value.
// Matches lines like:
//
// #$Env:WEBI_VERSION = v12.16.2
// $Env:WEBI_HOST = 'https://webinstall.dev'
func InjectPSVar(text, name, value string) string {
p := getPSVarPattern(name)
return p.ReplaceAllString(text, "${1}$$Env:"+name+" = '"+sanitizePSValue(value)+"'")
}
var psVarPatterns = map[string]*regexp.Regexp{}
func getPSVarPattern(name string) *regexp.Regexp {
if p, ok := psVarPatterns[name]; ok {
return p
}
// Match: optional leading whitespace, optional #, $Env:NAME, =, rest of line
p := regexp.MustCompile(`(?m)^([ \t]*)#?\$Env:` + regexp.QuoteMeta(name) + `\s*=.*$`)
psVarPatterns[name] = p
return p
}
// sanitizePSValue escapes single quotes for PowerShell single-quoted strings.
// In PowerShell, single quotes inside single-quoted strings are doubled: ''
func sanitizePSValue(s string) string {
return strings.ReplaceAll(s, "'", "''")
}
// varPattern matches shell variable declarations in the template.
// Matches lines like:
//