Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/windows #46

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 59 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
name: CI
name: Build & Release

on:
pull_request: {}
push: {}
workflow_dispatch:
inputs: {}

permissions:
contents: write
packages: write

jobs:
ci:
name: CI
build-binary:
name: build-binary
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -16,7 +22,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.19'
go-version: '1.22'
check-latest: true
cache: true
- name: Build
Expand All @@ -31,4 +37,52 @@ jobs:
version: latest
args: release --rm-dist --config .github/goreleaser.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

docker-build:
runs-on: ubuntu-latest
name: Deploy to Docker Image
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create Image Tag from branch name
if: startsWith(github.ref, 'refs/heads/release')
run: |
set +e
IMAGE_TAG=$(echo ${GITHUB_REF#refs/heads/} | sed 's/release-//g')
echo "$IMAGE_TAG" | grep -i '\-nightly$'
if [ $? -ne 0 ]; then
IMAGE_TAG="$IMAGE_TAG-nightly"
fi
set -e

echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
echo "OVERRIDE_PUSHED_IMAGE=true" >> $GITHUB_ENV

- name: Create Image Tag from tag
if: startsWith(github.ref, 'refs/tags/')
run: |
IMAGE_TAG=$(echo ${GITHUB_REF#refs/tags/})

echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
echo "OVERRIDE_PUSHED_IMAGE=false" >> $GITHUB_ENV

- name: Build & Push Image
if: startsWith(github.ref, 'refs/heads/release') || startsWith(github.ref, 'refs/tags/')
run: |
image_name="ghcr.io/${{ github.repository }}"

docker build -t $image_name:$IMAGE_TAG .
docker push $image_name:$IMAGE_TAG

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tmp/
test.sh
bin
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# syntax=docker/dockerfile:1.4
FROM golang:1.22.0-alpine3.18 AS base
USER 1001
ENV GOPATH=/tmp/go
ENV GOCACHE=/tmp/go-cache
WORKDIR /tmp/app
COPY . .
RUN go mod download -x

RUN CGO_ENABLED=0 go build -o /tmp/bin/bin-installer ./main.go
RUN chmod +x /tmp/bin/bin-installer

FROM gcr.io/distroless/static-debian11:nonroot
LABEL org.opencontainers.image.source=https://github.com/kloudlite/bin-installer
COPY --from=base /tmp/bin/bin-installer ./bin-installer
CMD ["./bin-installer"]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ curl https://i.jpillora.com/<query>! | bash

*Or you can use* `wget -qO- <url> | bash`

*For windows use* `iwr <url> | iex`

**Path API**

* `user` Github user (defaults to @jpillora, customisable if you [host your own](#host-your-own), searches the web to pick most relevant `user` when `repo` not found)
Expand All @@ -36,6 +38,8 @@ curl https://i.jpillora.com/<query>! | bash
* `type=homebrew` is **not** working at the moment – see [Homebrew](#homebrew)
* `?insecure=1` Force `curl`/`wget` to skip certificate checks
* `?as=` Force the binary to be named as this parameter value
* `?select=` Select binary, if **repository name** and **binary name** in release differs.
* **eg**: repo_name is **foobar** and binary name is **fb-client** and **fb-server** in release, Then `?select=fb-client` & `?select=fb-server` accordingly.

## Security

Expand Down
12 changes: 12 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 3

tasks:
run:
cmds:
- go run main.go
dev:
cmds:
- nodemon -q -e 'go,tmpl' --signal SIGTERM --exec "echo '# building' && task build && echo '# build success' && ./bin/installer || exit"
build:
cmds:
- go build -o ./bin/installer main.go
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/jpillora/installer

go 1.18
go 1.19

require (
github.com/jpillora/opts v1.1.2
Expand Down
31 changes: 24 additions & 7 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ const (
var (
isTermRe = regexp.MustCompile(`(?i)^(curl|wget)\/`)
isHomebrewRe = regexp.MustCompile(`(?i)^homebrew`)
isPowershell = regexp.MustCompile(`(?i)windows`)
errMsgRe = regexp.MustCompile(`[^A-Za-z0-9\ :\/\.]`)
errNotFound = errors.New("not found")
)

type Query struct {
User, Program, AsProgram, Release string
MoveToPath, Search, Insecure bool
SudoMove bool // deprecated: not used, now automatically detected
User, Program, AsProgram, Selected, Release string
MoveToPath, Search, Insecure bool
SudoMove bool // deprecated: not used, now automatically detected
}

type Result struct {
Expand Down Expand Up @@ -76,12 +77,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
qtype = "script"
case isHomebrewRe.MatchString(ua):
qtype = "ruby"
case isPowershell.MatchString(ua):
qtype = "powershell"
default:
qtype = "text"
}
}
// type specific error response
showError := func(msg string, code int) {
showError := func(msg string, _ int) {
// prevent shell injection
cleaned := errMsgRe.ReplaceAllString(msg, "")
if qtype == "script" {
Expand All @@ -102,6 +105,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
ext = "txt"
script = string(scripts.Text)
case "powershell":
w.Header().Set("Content-Type", "text/plain")
ext = "ps1"
script = string(scripts.Powershell)
default:
showError("Unknown type", http.StatusInternalServerError)
return
Expand All @@ -110,6 +117,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
User: "",
Program: "",
Release: "",
Selected: r.URL.Query().Get("select"),
Insecure: r.URL.Query().Get("insecure") == "1",
AsProgram: r.URL.Query().Get("as"),
}
Expand All @@ -120,9 +128,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
q.MoveToPath = true
path = strings.TrimRight(path, "!")
}
var rest string
q.User, rest = splitHalf(path, "/")
q.Program, q.Release = splitHalf(rest, "@")

var initial string
initial, q.Release = splitHalf(path, "@")
q.User, q.Program = splitHalf(initial, "/")

// change binary name to selected-binary
if q.AsProgram == "" && q.Selected != "" {
q.AsProgram = q.Selected
}

// no program? treat first part as program, use default user
if q.Program == "" {
q.Program = q.User
Expand All @@ -143,6 +158,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.Config.ForceRepo != "" {
q.Program = h.Config.ForceRepo
}

// validate query
valid := q.Program != ""
if !valid && path == "" {
Expand All @@ -160,6 +176,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
showError(err.Error(), http.StatusBadGateway)
return
}

// load template
t, err := template.New("installer").Parse(script)
if err != nil {
Expand Down
19 changes: 15 additions & 4 deletions handler/handler_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/http"
"slices"
"strings"
"time"
)
Expand Down Expand Up @@ -117,9 +118,18 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
//only binary containers are supported
//TODO deb,rpm etc
fext := getFileExt(url)

if q.Selected != "" {
//filter binary with it's name
if len(ga.Name) > len(q.Selected)+1 && !strings.Contains(ga.Name[0:len(q.Selected)+1], fmt.Sprint(q.Selected, "-")) {
continue
}
}

if fext == "" && ga.Size > 1024*1024 {
fext = ".bin" // +1MB binary
}

switch fext {
case ".bin", ".zip", ".tar.bz", ".tar.bz2", ".bz2", ".gz", ".tar.gz", ".tgz":
// valid
Expand All @@ -132,10 +142,11 @@ func (h *Handler) getAssetsNoCache(q Query) (string, Assets, error) {
arch := getArch(ga.Name)
//windows not supported yet
if os == "windows" {
log.Printf("fetched asset is for windows: %s", ga.Name)
//TODO: powershell
// EG: iwr https://deno.land/x/install/install.ps1 -useb | iex
continue
// with windows system commands, only zip can be extracted
if !slices.Contains([]string{".zip"}, fext) {
log.Printf("windows don't support fileextension %s ", fext)
continue
}
}
//unknown os, cant use
if os == "" {
Expand Down
108 changes: 108 additions & 0 deletions scripts/install.ps1.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
$user = "{{.User}}"
$prog="{{ .Program }}"
$sel_bin="{{ .Selected }}"
$asProgram="{{ .AsProgram }}"
$move="{{ .MoveToPath }}"
$release="{{ .Release }}"
$insecure="{{ .Insecure }}"

$arch = "$env:PROCESSOR_ARCHITECTURE".ToLower()
$url = ""
$fext = ""

{{- range .Assets}} {{- if eq .OS "windows" }}
if ("{{.Arch}}" -eq $arch -and "{{.Type}}" -eq ".zip"){
$url = "{{.URL}}"
$fext = "{{.Type}}"
}
{{- end}} {{- end}}

if($url -eq ""){
echo "No asset for platform windows-$arch"
return
}


if ($move -eq "true"){
# $out_dir="$env:USERPROFILE\bin"
$out_dir="C:\bin"
}else{
$out_dir="$PWD"
}

# Create the destination directory if it doesn't exist
if (-not (Test-Path $out_dir)) {
New-Item -ItemType Directory -Force -Path $out_dir
}

$filename = ""

# to extract another file type rather than zip, third party application is needed.
# so as windows only capable of extracting zip file with system commands.
switch ($fext)
{
.zip {
$filename = "app-$prog-$arch.zip";
}
}

if("" -eq $filename){
echo "file extension $fext not supported"
return
}

$zipFilePath = "$env:TEMP\$filename";
$extPath = "$env:TEMP\$prog"

# Downloading File
if($sel_bin){
echo "[#] downloading $user/$prog/$sel_bin as $asProgram from $url"
}else{
echo "[#] downloading $user/$prog as $asProgram from $url"
}

Invoke-WebRequest -Uri $url -OutFile $zipFilePath
# Extracting zip
echo "[#] extracting file"
Expand-Archive -Path $zipFilePath -DestinationPath $extPath
# Moving To out_dir
Get-ChildItem -Path $extPath -Filter *.exe -Recurse | Move-Item -Destination $out_dir -Force
# Clean up the downloaded ZIP and temporary extracted folder
Remove-Item -Path $zipFilePath -Force
Remove-Item -Path $extPath -Recurse -Force

echo "[#] downloaded successfully to path $out_dir"
if ($move -eq "false"){
return
}

echo "[#] setting $out_dir to path"
# Get the current user's PATH environment variable
$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", [System.EnvironmentVariableTarget]::User)

# Split the PATH variable into an array of individual paths
$pathArray = $currentPath -split ";"

$hasPath = "false"
# Iterate over each path in the PATH variable
foreach ($path in $pathArray) {
# Check if the current path contains the specific directory
if ($path -eq $out_dir) {
$hasPath = "true"
}
}

if ($hasPath -eq "false") {
# Update the PATH environment variable
if (-not [string]::IsNullOrWhiteSpace($currentPath)) {
$updatedPath = $currentPath + ";" + $out_dir
} else {
$updatedPath = $out_dir
}

# Set the updated PATH
[System.Environment]::SetEnvironmentVariable("PATH", $updatedPath, [System.EnvironmentVariableTarget]::User)
$env:Path = $out_dir
}

echo "[#] installation complete"
Loading