Skip to content

Commit a11a2c8

Browse files
committed
chore: upgrade ghreleases
1 parent f2f1795 commit a11a2c8

File tree

3 files changed

+397
-1
lines changed

3 files changed

+397
-1
lines changed

compiler/internal/installer/githubreleases/githubreleases_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
func TestGHInstaller(t *testing.T) {
1919
ghr := &ghReleasesInstaller{
2020
config: map[string]string{
21-
"owner": `MeteorsLiu`,
21+
"owner": `goplus`,
2222
"repo": `llpkg`,
2323
"platform": runtime.GOOS,
2424
"arch": runtime.GOARCH,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package githubreleases
2+
3+
import (
4+
"archive/tar"
5+
"archive/zip"
6+
"bytes"
7+
"compress/gzip"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"os"
13+
"path/filepath"
14+
"strings"
15+
"text/template"
16+
17+
"github.com/goplus/llpkgstore/upstream"
18+
)
19+
20+
var ErrPackageNotFound = errors.New("package not found")
21+
22+
// ghReleasesInstaller implements the upstream.Installer interface by downloading
23+
// the corresponding package from a GitHub release.
24+
type ghReleasesInstaller struct {
25+
config map[string]string
26+
}
27+
28+
// NewGHReleasesInstaller creates a new GitHub Release installer with the specified configuration.
29+
// The config map is the info of release repo, for example:
30+
// "owner": `goplus`,
31+
// "repo": `llpkg`,
32+
// "platform": runtime.GOOS,
33+
// "arch": runtime.GOARCH,
34+
func NewGHReleasesInstaller(config map[string]string) upstream.Installer {
35+
return &ghReleasesInstaller{
36+
config: config,
37+
}
38+
}
39+
40+
func (c *ghReleasesInstaller) Name() string {
41+
return "ghreleases"
42+
}
43+
44+
func (c *ghReleasesInstaller) Config() map[string]string {
45+
return c.config
46+
}
47+
48+
// Install downloads the package from the GitHub Release and extracts it to the output directory.
49+
// Unlike conaninstaller which is used for GitHub Action to obtain binary files
50+
// this installer is used for `llgo get` to install binary files.
51+
// The first return value is an empty string, as the pkgConfigName is not necessary for this GitHub Release installer.
52+
func (c *ghReleasesInstaller) Install(pkg upstream.Package, outputDir string) ([]string, error) {
53+
compressPath, err := c.download(c.assertUrl(pkg), outputDir)
54+
if err != nil {
55+
return nil, err
56+
}
57+
if strings.HasSuffix(compressPath, ".tar.gz") {
58+
err = c.untargz(outputDir, compressPath)
59+
if err != nil {
60+
return nil, err
61+
}
62+
} else if strings.HasSuffix(compressPath, ".zip") {
63+
err = c.unzip(outputDir, compressPath)
64+
if err != nil {
65+
return nil, err
66+
}
67+
} else {
68+
return nil, errors.New("unsupported compressed file format")
69+
}
70+
err = os.Remove(compressPath)
71+
if err != nil {
72+
return nil, fmt.Errorf("cannot delete compressed file: %w", err)
73+
}
74+
err = c.setPrefix(outputDir)
75+
if err != nil {
76+
return nil, fmt.Errorf("fail to reset .pc prefix: %w", err)
77+
}
78+
return nil, nil
79+
}
80+
81+
// Warning: not implemented
82+
// Search is unnecessary for this installer
83+
func (c *ghReleasesInstaller) Search(pkg upstream.Package) ([]string, error) {
84+
return nil, errors.New("unimplemented")
85+
}
86+
87+
// assertUrl returns the URL for the specified package.
88+
// The URL is constructed based on the package name, version, and the installer configuration.
89+
func (c *ghReleasesInstaller) assertUrl(pkg upstream.Package) string {
90+
releaseName := fmt.Sprintf("%s/%s", pkg.Name, pkg.Version)
91+
fileName := fmt.Sprintf("%s_%s.zip", pkg.Name, c.config["platform"]+"_"+c.config["arch"])
92+
return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s", c.config["owner"], c.config["repo"], releaseName, fileName)
93+
}
94+
95+
// download fetches the package from the specified URL and saves it to the output directory.
96+
func (c *ghReleasesInstaller) download(url string, outputDir string) (string, error) {
97+
client := &http.Client{}
98+
resp, err := client.Get(url)
99+
if err != nil {
100+
return "", err
101+
}
102+
defer resp.Body.Close()
103+
if resp.StatusCode == http.StatusNotFound {
104+
return "", ErrPackageNotFound
105+
}
106+
if resp.StatusCode != http.StatusOK {
107+
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
108+
}
109+
110+
parts := strings.Split(url, "/")
111+
filename := parts[len(parts)-1]
112+
113+
err = os.MkdirAll(outputDir, 0755)
114+
if err != nil {
115+
return "", err
116+
}
117+
118+
outputPath := filepath.Join(outputDir, filename)
119+
outFile, err := os.Create(outputPath)
120+
if err != nil {
121+
return "", err
122+
}
123+
defer outFile.Close()
124+
125+
_, err = io.Copy(outFile, resp.Body)
126+
if err != nil {
127+
return "", err
128+
}
129+
130+
return outputPath, nil
131+
}
132+
133+
// untargz extracts the gzip-compressed tarball to the output directory.
134+
// The gzipPath must be a .tar.gz file.
135+
func (c *ghReleasesInstaller) untargz(outputDir string, gzipPath string) error {
136+
fr, err := os.Open(gzipPath)
137+
if err != nil {
138+
return err
139+
}
140+
defer fr.Close()
141+
142+
gr, err := gzip.NewReader(fr)
143+
if err != nil {
144+
return err
145+
}
146+
defer gr.Close()
147+
148+
tr := tar.NewReader(gr)
149+
for {
150+
header, err := tr.Next()
151+
if err == io.EOF {
152+
break
153+
}
154+
if err != nil {
155+
return err
156+
}
157+
158+
path := filepath.Join(outputDir, header.Name)
159+
info := header.FileInfo()
160+
161+
if info.IsDir() {
162+
if err = os.MkdirAll(path, info.Mode()); err != nil {
163+
return err
164+
}
165+
continue
166+
}
167+
dir := filepath.Dir(path)
168+
if err = os.MkdirAll(dir, 0755); err != nil {
169+
return err
170+
}
171+
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
172+
if err != nil {
173+
return err
174+
}
175+
if _, err = io.Copy(file, tr); err != nil {
176+
return err
177+
}
178+
file.Close()
179+
}
180+
return nil
181+
}
182+
183+
// unzip extracts the zip file to the output directory.
184+
// The zipPath must be a .zip file.
185+
func (c *ghReleasesInstaller) unzip(outputDir string, zipPath string) error {
186+
r, err := zip.OpenReader(zipPath)
187+
if err != nil {
188+
return err
189+
}
190+
defer r.Close()
191+
decompress := func(file *zip.File) error {
192+
path := filepath.Join(outputDir, file.Name)
193+
194+
if file.FileInfo().IsDir() {
195+
if err := os.MkdirAll(path, 0755); err != nil {
196+
return err
197+
}
198+
return nil
199+
}
200+
fs, err := file.Open()
201+
if err != nil {
202+
return err
203+
}
204+
w, err := os.Create(path)
205+
if err != nil {
206+
return err
207+
}
208+
defer fs.Close()
209+
if _, err := io.Copy(w, fs); err != nil {
210+
w.Close()
211+
return err
212+
}
213+
return w.Close()
214+
}
215+
216+
for _, file := range r.File {
217+
if err = decompress(file); err != nil {
218+
break
219+
}
220+
}
221+
return err
222+
}
223+
224+
// setPrefix can generate .pc files from .pc.tmpl files
225+
func (c *ghReleasesInstaller) setPrefix(outputDir string) error {
226+
absOutputDir, err := filepath.Abs(outputDir)
227+
if err != nil {
228+
return err
229+
}
230+
231+
// move to path where .pc files are stored
232+
pkgConfigPath := filepath.Join(outputDir, "lib", "pkgconfig")
233+
234+
pcTmpls, err := filepath.Glob(filepath.Join(pkgConfigPath, "*.pc.tmpl"))
235+
if err != nil {
236+
return err
237+
}
238+
if len(pcTmpls) == 0 {
239+
return errors.New("no .pc.tmpl files found")
240+
}
241+
for _, pcTmpl := range pcTmpls {
242+
tmplContent, err := os.ReadFile(pcTmpl)
243+
if err != nil {
244+
return err
245+
}
246+
tmplName := filepath.Base(pcTmpl)
247+
tmpl, err := template.New(tmplName).Parse(string(tmplContent))
248+
if err != nil {
249+
return err
250+
}
251+
252+
pcFilePath := filepath.Join(pkgConfigPath, strings.TrimSuffix(tmplName, ".tmpl"))
253+
var buf bytes.Buffer
254+
// The Prefix field specifies the absolute path to the output directory,
255+
// which is used to replace placeholders in the .pc template files.
256+
if err := tmpl.Execute(&buf, map[string]any{
257+
"Prefix": absOutputDir,
258+
}); err != nil {
259+
return err
260+
}
261+
if err := os.WriteFile(pcFilePath, buf.Bytes(), 0644); err != nil {
262+
return err
263+
}
264+
// remove .pc.tmpl file
265+
err = os.Remove(filepath.Join(pkgConfigPath, tmplName))
266+
if err != nil {
267+
return fmt.Errorf("failed to remove template file: %w", err)
268+
}
269+
}
270+
271+
return nil
272+
}

0 commit comments

Comments
 (0)