mirror of
https://github.com/webinstall/webi-installers.git
synced 2026-04-06 18:36:50 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user