-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtwshim.go
168 lines (145 loc) · 4.13 KB
/
twshim.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package twshim
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path"
"runtime"
"strconv"
)
const baseReleaseURL = "https://api.github.com/repos/tailwindlabs/tailwindcss/releases" // without trailing slash
// Logger interface used by twshim to log. Set Log to the desired Logger.
type Logger interface {
Printf(format string, v ...any)
}
// make sure, log.Logger satisfies our interface
var _ Logger = &log.Logger{}
// Log is the Logger used by twshim. If Log = nil, twshim will use a log.Logger by default.
// To disable logging, set it to log.New(io.Discard, "", 0).
var Log Logger
func l() Logger {
if Log == nil {
Log = log.New(os.Stderr, "twshim: ", log.LstdFlags)
}
return Log
}
// Command returns an exec.Cmd ready for execution.
// If the binary of the given release does not exist in downloadRoot, it is downloaded first.
func Command(downloadRoot, releaseTag, assetName string, arg ...string) (*exec.Cmd, error) {
dir := path.Join(downloadRoot, releaseTag)
bin := path.Join(dir, assetName)
if _, err := os.Stat(bin); os.IsNotExist(err) {
l().Printf("downloading %s", bin)
if err := os.MkdirAll(dir, 0777); err != nil {
return nil, err
}
if err := DownloadCLI(releaseTag, assetName, bin); err != nil {
return nil, fmt.Errorf("downloading tailwind %s: %w", releaseTag, err)
}
} else if err != nil {
return nil, err
}
cmd := exec.Command(bin, arg...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd, nil
}
// RuntimeAssetName derives the name of the GitHub asset that holds the tailwindcss standalone CLI binary for
// the operating system and architecture this func is called on.
func RuntimeAssetName() (string, error) {
dist := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
// Add more distributions as needed.
switch dist {
case "linux/amd64":
return "tailwindcss-linux-x64", nil
case "windows/amd64":
return "tailwindcss-windows-x64.exe", nil
default:
return "", fmt.Errorf("distribution not supported: %s", dist)
}
}
// DownloadCLI downloads a named asset from a release by tag to the desired destination and makes it executable.
func DownloadCLI(tag, asset, dest string) error {
rURL := baseReleaseURL + "/tags/" + tag
r, err := fetchRelease(rURL)
if err != nil {
return err
}
a, err := r.findAsset(asset)
if err != nil {
return err
}
aURL := baseReleaseURL + "/assets/" + strconv.Itoa(a.ID)
if err := downloadFile(aURL, dest); err != nil {
return err
}
// Make it executable by user and group.
err = os.Chmod(dest, 0770)
if err != nil {
return err
}
return nil
}
func fetchRelease(url string) (githubRelease, error) {
resp, err := http.Get(url)
if err != nil {
return githubRelease{}, err
}
//goland:noinspection GoUnhandledErrorResult
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return githubRelease{}, fmt.Errorf("bad status: %s", resp.Status)
}
var r githubRelease
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return githubRelease{}, err
}
return r, nil
}
type githubRelease struct {
Name string `json:"name"`
Assets []githubAsset `json:"assets"`
Tag string `json:"tag_name"`
}
func (r *githubRelease) findAsset(name string) (githubAsset, error) {
for _, a := range r.Assets {
if a.Name == name {
return a, nil
}
}
return githubAsset{}, fmt.Errorf("no such asset: %s", name)
}
type githubAsset struct {
ID int `json:"id"`
Name string `json:"name"`
}
func downloadFile(url string, dest string) error {
req, err := http.NewRequest(http.MethodGet, url, nil)
// Setting Accept header to application/octet-stream instructs GitHub's API to send the asset's binary.
req.Header.Set("Accept", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
//goland:noinspection GoUnhandledErrorResult
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
out, err := os.Create(dest)
if err != nil {
return err
}
//goland:noinspection GoUnhandledErrorResult
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}