rm xtool/{cppkg,cpgithubpkg}
This commit is contained in:
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cpgithubpkg
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
type version struct {
|
||||
Folder string `yaml:"folder"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Versions map[string]version `yaml:"versions"`
|
||||
}
|
||||
|
||||
type template struct {
|
||||
FromVer string `yaml:"from"`
|
||||
Folder string `yaml:"folder"`
|
||||
Tag string `yaml:"tag,omitempty"` // pattern with *, empty if dynamic tag
|
||||
}
|
||||
|
||||
type configEx struct {
|
||||
PkgName string `yaml:"name"`
|
||||
Versions map[string]version `yaml:"versions"`
|
||||
Template template `yaml:"template"`
|
||||
}
|
||||
|
||||
// Main is the entry point for copying GitHub packages.
|
||||
func Main(pkgName string) {
|
||||
localDir := conanRoot() + "recipes/" + pkgName + "/"
|
||||
|
||||
confFile := localDir + "config.yml"
|
||||
b, err := os.ReadFile(confFile)
|
||||
check(err)
|
||||
|
||||
var conf config
|
||||
err = yaml.Unmarshal(b, &conf)
|
||||
check(err)
|
||||
|
||||
tryCp := func(src map[string]any, ver string, v version) {
|
||||
switch url := src["url"].(type) {
|
||||
case string:
|
||||
if pkgPath, tagPattern, ok := checkGithbPkg(url, ver); ok {
|
||||
cpGithubPkg(pkgName, pkgPath, tagPattern, localDir, conf, ver, v)
|
||||
}
|
||||
case []any:
|
||||
for _, u := range url {
|
||||
url := u.(string)
|
||||
if pkgPath, tagPattern, ok := checkGithbPkg(url, ver); ok {
|
||||
cpGithubPkg(pkgName, pkgPath, tagPattern, localDir, conf, ver, v)
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Println("[INFO] skip source:", src)
|
||||
}
|
||||
}
|
||||
|
||||
conandatas := make(map[string]conandata) // folder -> conandata
|
||||
rangeVerDesc(conf.Versions, func(ver string, v version) {
|
||||
cd, err := getConanData(conandatas, v.Folder, localDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
check(err)
|
||||
}
|
||||
|
||||
if src, ok := cd.Sources[ver]; ok {
|
||||
switch src := src.(type) {
|
||||
case map[string]any:
|
||||
tryCp(src, ver, v)
|
||||
case []any:
|
||||
for _, u := range src {
|
||||
tryCp(u.(map[string]any), ver, v)
|
||||
}
|
||||
default:
|
||||
log.Panicln("[FATAL] source:", src)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func cpGithubPkg(pkgName, pkgPath, tagPattern, srcDir string, conf config, fromVer string, v version) {
|
||||
destDir := cppkgRoot() + pkgPath
|
||||
os.MkdirAll(destDir, os.ModePerm)
|
||||
|
||||
err := exec.Command("cp", "-r", srcDir, destDir).Run()
|
||||
check(err)
|
||||
|
||||
confex := &configEx{
|
||||
PkgName: pkgName,
|
||||
Versions: conf.Versions,
|
||||
Template: template{
|
||||
FromVer: fromVer,
|
||||
Folder: v.Folder,
|
||||
Tag: tagPattern,
|
||||
},
|
||||
}
|
||||
b, err := yaml.Marshal(confex)
|
||||
check(err)
|
||||
|
||||
err = os.WriteFile(destDir+"/config.yml", b, os.ModePerm)
|
||||
check(err)
|
||||
|
||||
log.Println("[INFO] copy", pkgPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func checkGithbPkg(url, ver string) (pkgPath, tagPattern string, ok bool) {
|
||||
const githubPrefix = "https://github.com/"
|
||||
if strings.HasPrefix(url, githubPrefix) {
|
||||
path := url[len(githubPrefix):]
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
if len(parts) == 3 { // user/repo/xxx
|
||||
if pos := strings.Index(parts[2], ver); pos >= 0 {
|
||||
userRepo := parts[0] + "/" + parts[1]
|
||||
at := len(githubPrefix) + len(userRepo) + 1 + pos
|
||||
tagPattern = tagPatternOf(url, ver, at)
|
||||
pkgPath, ok = strings.ToLower(userRepo), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func tagPatternOf(url, ver string, at int) (tagPattern string) {
|
||||
var tag string
|
||||
if pos := strings.LastIndexByte(url[:at], '/'); pos >= 0 {
|
||||
last := at + len(ver)
|
||||
left := url[last:]
|
||||
if end, ok := checkTagEnd(left); ok {
|
||||
pos++
|
||||
tag = url[pos:last]
|
||||
tagPattern = url[pos:at] + end
|
||||
}
|
||||
}
|
||||
if tag == "" {
|
||||
log.Println("[INFO] dynamic tag found:", url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkTagEnd(left string) (end string, ok bool) {
|
||||
if n := len(left); n > 0 {
|
||||
if left[0] == '/' {
|
||||
return "*", true
|
||||
}
|
||||
if n >= 4 && left[0] == '.' {
|
||||
ext := left[1:4]
|
||||
return "*", ext == "tar" || ext == "zip" || ext == "tgz"
|
||||
}
|
||||
if strings.HasPrefix(left, "-stable.") {
|
||||
return "*-stable", true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
type conandata struct {
|
||||
Sources map[string]any `yaml:"sources"`
|
||||
}
|
||||
|
||||
func getConanData(conandatas map[string]conandata, folder, localDir string) (ret conandata, err error) {
|
||||
if v, ok := conandatas[folder]; ok {
|
||||
return v, nil
|
||||
}
|
||||
file := localDir + folder + "/conandata.yml"
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = yaml.Unmarshal(b, &ret); err != nil {
|
||||
return
|
||||
}
|
||||
conandatas[folder] = ret
|
||||
return
|
||||
}
|
||||
|
||||
func rangeVerDesc[V any](data map[string]V, f func(string, V)) {
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, "v"+k)
|
||||
}
|
||||
semver.Sort(keys)
|
||||
for _, k := range slices.Backward(keys) {
|
||||
k = k[1:] // remove 'v'
|
||||
f(k, data[k])
|
||||
}
|
||||
}
|
||||
|
||||
func conanRoot() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/conan-center-index/"
|
||||
}
|
||||
|
||||
func cppkgRoot() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/cppkg/"
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cppkg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound is the error resulting if a path search failed to find
|
||||
// an executable file.
|
||||
ErrNotFound = exec.ErrNotFound
|
||||
)
|
||||
|
||||
// Tool represents a tool that can be executed.
|
||||
type Tool struct {
|
||||
cmd string
|
||||
installs [][]string
|
||||
}
|
||||
|
||||
// NewTool creates a new Tool instance with the specified tool and install commands.
|
||||
func NewTool(cmd string, installs []string) *Tool {
|
||||
inst := make([][]string, len(installs))
|
||||
for i, install := range installs {
|
||||
inst[i] = strings.Split(install, " ")
|
||||
}
|
||||
return &Tool{
|
||||
cmd: cmd,
|
||||
installs: inst,
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new command with the specified arguments.
|
||||
func (p *Tool) New(quietInstall bool, args ...string) (cmd *exec.Cmd, err error) {
|
||||
app, err := p.Get(quietInstall)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return exec.Command(app, args...), nil
|
||||
}
|
||||
|
||||
// Get retrieves the path of the command.
|
||||
// If the command is not found, it attempts to install it using the specified
|
||||
// install commands.
|
||||
func (p *Tool) Get(quietInstall bool) (app string, err error) {
|
||||
app, err = exec.LookPath(p.cmd)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
amPath, install, err := p.getAppManager()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c := exec.Command(amPath, install[1:]...)
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
if err = c.Run(); err != nil {
|
||||
return
|
||||
}
|
||||
return exec.LookPath(p.cmd)
|
||||
}
|
||||
|
||||
func (p *Tool) getAppManager() (amPath string, install []string, err error) {
|
||||
for _, install = range p.installs {
|
||||
am := install[0]
|
||||
if amPath, err = exec.LookPath(am); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = ErrNotFound
|
||||
return
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cppkg
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goplus/llgo/internal/github"
|
||||
"github.com/qiniu/x/byteutil"
|
||||
"github.com/qiniu/x/httputil"
|
||||
)
|
||||
|
||||
var conanCmd = NewTool("conan", []string{
|
||||
"brew install conan",
|
||||
"pipx install conan",
|
||||
})
|
||||
|
||||
type conandata struct {
|
||||
Sources map[string]any `yaml:"sources"`
|
||||
}
|
||||
|
||||
func replaceVer(src any, fromVer, toVer string) any {
|
||||
switch src := src.(type) {
|
||||
case map[string]any:
|
||||
doReplace(src, fromVer, toVer)
|
||||
case []any:
|
||||
for _, u := range src {
|
||||
doReplace(u.(map[string]any), fromVer, toVer)
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func doReplace(src map[string]any, fromVer, toVer string) {
|
||||
switch url := src["url"].(type) {
|
||||
case string:
|
||||
src["url"] = strings.ReplaceAll(url, fromVer, toVer)
|
||||
delete(src, "sha256")
|
||||
// TODO(xsw): src["sha256"] = hash
|
||||
case []any:
|
||||
for i, u := range url {
|
||||
url[i] = strings.ReplaceAll(u.(string), fromVer, toVer)
|
||||
}
|
||||
delete(src, "sha256")
|
||||
// TODO(xsw): src["sha256"] = hash
|
||||
}
|
||||
}
|
||||
|
||||
type githubRelease struct {
|
||||
PublishedAt string
|
||||
}
|
||||
|
||||
func getRelease(pkg *Package, tagPattern string) (ret *githubRelease, err error) {
|
||||
if tagPattern == "" {
|
||||
return nil, ErrDynamicTag
|
||||
}
|
||||
if pkg.gr != nil {
|
||||
return &githubRelease{PublishedAt: pkg.gr.PublishedAt}, nil
|
||||
}
|
||||
ver := strings.Replace(tagPattern, "*", pkg.Version, 1)
|
||||
gr, err := github.GetRelease(pkg.Path, ver)
|
||||
if err == nil {
|
||||
ret = &githubRelease{PublishedAt: gr.PublishedAt}
|
||||
return
|
||||
}
|
||||
t, err := github.GetTag(pkg.Path, ver)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c, err := github.GetCommit(pkg.Path, t.Commit.URL)
|
||||
if err == nil {
|
||||
ret = &githubRelease{PublishedAt: c.Commit.Author.Date}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Install installs the specified package using Conan.
|
||||
func (p *Manager) Install(pkg *Package, options []string, flags int) (buildDir string, err error) {
|
||||
buildDir = p.BuildDir(pkg, options)
|
||||
os.MkdirAll(buildDir, os.ModePerm)
|
||||
|
||||
var rev string
|
||||
var gr *githubRelease
|
||||
var conandataYml, conanfilePy []byte
|
||||
|
||||
conanfileDir := p.conanfileDir(pkg.Path, pkg.Folder)
|
||||
pkgVer := pkg.Version
|
||||
template := pkg.Template
|
||||
if template != nil {
|
||||
gr, err = getRelease(pkg, template.Tag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = copyDirR(conanfileDir, buildDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conanfilePy, err = os.ReadFile(buildDir + "/conanfile.py")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conandataFile := buildDir + "/conandata.yml"
|
||||
conandataYml, err = os.ReadFile(conandataFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var cd conandata
|
||||
err = yaml.Unmarshal(conandataYml, &cd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fromVer := template.FromVer
|
||||
source, ok := cd.Sources[fromVer]
|
||||
if !ok {
|
||||
return "", ErrVersionNotFound
|
||||
}
|
||||
cd.Sources = map[string]any{
|
||||
pkgVer: replaceVer(source, fromVer, pkgVer),
|
||||
}
|
||||
conandataYml, err = yaml.Marshal(cd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(conandataFile, conandataYml, os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rev = recipeRevision(pkg, gr, conandataYml)
|
||||
conanfileDir = buildDir
|
||||
}
|
||||
|
||||
outFile := buildDir + "/out.json"
|
||||
out, err := os.Create(outFile)
|
||||
if err == nil {
|
||||
defer out.Close()
|
||||
} else {
|
||||
out = os.Stdout
|
||||
}
|
||||
|
||||
nameAndVer := pkg.Name + "/" + pkgVer
|
||||
if template == nil {
|
||||
err = conanInstall(nameAndVer, buildDir, conanfileDir, out, options, flags)
|
||||
return
|
||||
}
|
||||
|
||||
logFile := ""
|
||||
if flags&LogRevertProxy != 0 {
|
||||
logFile = buildDir + "/rp.log"
|
||||
}
|
||||
err = remoteProxy(flags, logFile, func() error {
|
||||
return conanInstall(nameAndVer, buildDir, conanfileDir, out, options, flags)
|
||||
}, func(mux *http.ServeMux) {
|
||||
base := "/v2/conans/" + nameAndVer
|
||||
revbase := base + "/_/_/revisions/" + rev
|
||||
mux.HandleFunc(base+"/_/_/latest", func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("Cache-Control", "public,max-age=300")
|
||||
httputil.Reply(w, http.StatusOK, map[string]any{
|
||||
"revision": rev,
|
||||
"time": gr.PublishedAt,
|
||||
})
|
||||
})
|
||||
mux.HandleFunc(revbase+"/files", func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("Cache-Control", "public,max-age=3600")
|
||||
empty := map[string]any{}
|
||||
httputil.Reply(w, http.StatusOK, map[string]any{
|
||||
"files": map[string]any{
|
||||
"conan_export.tgz": empty,
|
||||
"conanmanifest.txt": empty,
|
||||
"conanfile.py": empty,
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc(revbase+"/files/conanfile.py", func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("Cache-Control", "public,max-age=3600")
|
||||
h.Set("Content-Disposition", `attachment; filename="conanfile.py"`)
|
||||
httputil.ReplyWith(w, http.StatusOK, "text/x-python", conanfilePy)
|
||||
})
|
||||
const conanmanifest = "%d\nconandata.yml: %s\nconanfile.py: %s\n"
|
||||
mux.HandleFunc(revbase+"/files/conanmanifest.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
mtime, err := unixTime(gr.PublishedAt)
|
||||
if err != nil {
|
||||
replyError(w, err)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
h.Set("Cache-Control", "public,max-age=3600")
|
||||
h.Set("Content-Disposition", `attachment; filename="conanmanifest.txt"`)
|
||||
data := fmt.Sprintf(conanmanifest, mtime, md5Of(conandataYml), md5Of(conanfilePy))
|
||||
httputil.ReplyWithStream(w, http.StatusOK, "text/plain", strings.NewReader(data), int64(len(data)))
|
||||
})
|
||||
mux.HandleFunc(revbase+"/files/conan_export.tgz", func(w http.ResponseWriter, r *http.Request) {
|
||||
conanExportTgz, err := tgzOfConandata(buildDir)
|
||||
if err != nil {
|
||||
replyError(w, err)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
h.Set("Cache-Control", "public,max-age=3600")
|
||||
h.Set("Content-Disposition", `attachment; filename="conan_export.tgz"`)
|
||||
httputil.ReplyWith(w, http.StatusOK, "application/x-gzip", conanExportTgz)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Manager) BuildDir(pkg *Package, options []string) string {
|
||||
dir := p.cacheDir + "/build/" + pkg.Name + "@" + pkg.Version
|
||||
if options != nil {
|
||||
h := md5.New()
|
||||
for _, opt := range options {
|
||||
h.Write(byteutil.Bytes(opt))
|
||||
}
|
||||
hash := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
dir += "/" + hash
|
||||
} else {
|
||||
dir += "/static"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func (p *Manager) conanfileDir(pkgPath, pkgFolder string) string {
|
||||
root := p.IndexRoot()
|
||||
return root + "/" + pkgPath + "/" + pkgFolder
|
||||
}
|
||||
|
||||
func conanInstall(pkg, outDir, conanfileDir string, out io.Writer, options []string, flags int) (err error) {
|
||||
args := make([]string, 0, 16)
|
||||
args = append(args, "install",
|
||||
"--requires", pkg,
|
||||
"--generator", "PkgConfigDeps",
|
||||
"--build", "missing",
|
||||
"--format", "json",
|
||||
"--output-folder", outDir,
|
||||
)
|
||||
for _, opt := range options {
|
||||
args = append(args, "--options", opt)
|
||||
}
|
||||
quietInstall := flags&ToolQuietInstall != 0
|
||||
cmd, err := conanCmd.New(quietInstall, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd.Dir = conanfileDir
|
||||
cmd.Stderr = ConanStderr
|
||||
cmd.Stdout = out
|
||||
err = cmd.Run()
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// ConanStderr is the standard error output for command conan.
|
||||
ConanStderr = os.Stderr
|
||||
)
|
||||
|
||||
func recipeRevision(_ *Package, _ *githubRelease, conandataYml []byte) string {
|
||||
return md5Of(conandataYml)
|
||||
}
|
||||
|
||||
func md5Of(data []byte) string {
|
||||
h := md5.New()
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func tgzOfConandata(outDir string) (_ []byte, err error) {
|
||||
cmd := exec.Command("tar", "-czf", "conan_export.tgz", "conandata.yml")
|
||||
cmd.Dir = outDir
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return os.ReadFile(outDir + "/conan_export.tgz")
|
||||
}
|
||||
|
||||
func unixTime(tstr string) (ret int64, err error) {
|
||||
t, err := time.Parse(time.RFC3339, tstr)
|
||||
if err == nil {
|
||||
ret = t.Unix()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func copyDirR(srcDir, destDir string) error {
|
||||
if cp, err := exec.LookPath("cp"); err == nil {
|
||||
return exec.Command(cp, "-r", "-p", srcDir+"/", destDir).Run()
|
||||
}
|
||||
if cp, err := exec.LookPath("xcopy"); err == nil {
|
||||
// TODO(xsw): check xcopy
|
||||
return exec.Command(cp, "/E", "/I", "/Y", srcDir+"/", destDir).Run()
|
||||
}
|
||||
return errors.New("copy command not found")
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cppkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultFlags is the default flags for package installation.
|
||||
DefaultFlags = IndexAutoUpdate | ToolQuietInstall
|
||||
)
|
||||
|
||||
// Install installs a package with the given name and version.
|
||||
// pkgAndVer: 7bitcoder/7bitconf@1.2.0
|
||||
func Install(pkgAndVer string, flags int) {
|
||||
pkgPath, ver := parsePkgVer(pkgAndVer)
|
||||
_, _, err := InstallPkg("", pkgPath, ver, nil, flags)
|
||||
check(err)
|
||||
}
|
||||
|
||||
// InstallPkg installs a package with the given package path and version.
|
||||
func InstallPkg(cacheDir, pkgPath, ver string, options []string, flags int) (pkg *Package, buildDir string, err error) {
|
||||
m, err := New(cacheDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pkg, err = m.Lookup(pkgPath, ver, flags)
|
||||
if err == nil {
|
||||
buildDir, err = m.Install(pkg, options, flags)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parsePkgVer(pkg string) (string, string) {
|
||||
parts := strings.SplitN(pkg, "@", 2)
|
||||
if len(parts) == 1 {
|
||||
return parts[0], ""
|
||||
}
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cppkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goplus/llgo/internal/github"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
var gitCmd = NewTool("git", []string{
|
||||
"brew install git",
|
||||
"apt-get install git",
|
||||
})
|
||||
|
||||
// Manager represents a package manager for C/C++ packages.
|
||||
type Manager struct {
|
||||
cacheDir string
|
||||
}
|
||||
|
||||
// New creates a new package manager.
|
||||
func New(cacheDir string) (ret *Manager, err error) {
|
||||
if cacheDir == "" {
|
||||
cacheDir = CacheDir()
|
||||
}
|
||||
os.MkdirAll(cacheDir, os.ModePerm)
|
||||
ret = &Manager{
|
||||
cacheDir: cacheDir,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CacheDir returns the cache directory to manage C/C++ packages.
|
||||
func CacheDir() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cacheDir + "/cppkg"
|
||||
}
|
||||
|
||||
type version struct {
|
||||
Folder string `yaml:"folder"`
|
||||
}
|
||||
|
||||
// Template represents a template for package versions.
|
||||
type Template struct {
|
||||
FromVer string `yaml:"from"`
|
||||
Folder string `yaml:"folder"`
|
||||
Tag string `yaml:"tag,omitempty"` // pattern with *, empty if dynamic tag
|
||||
}
|
||||
|
||||
type config struct {
|
||||
PkgName string `yaml:"name"`
|
||||
Versions map[string]version `yaml:"versions"`
|
||||
Template Template `yaml:"template"`
|
||||
}
|
||||
|
||||
// getKnownLatestVer returns the latest known version and its details.
|
||||
// It returns empty version if no known version is found.
|
||||
func (p *config) getKnownLatestVer() (ver string, v version) {
|
||||
for ver1, v1 := range p.Versions {
|
||||
if ver == "" || compareVer(ver1, ver) > 0 {
|
||||
ver, v = ver1, v1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Package represents a C/C++ package.
|
||||
type Package struct {
|
||||
Name string
|
||||
Path string
|
||||
Version string
|
||||
Folder string
|
||||
Template *Template
|
||||
|
||||
gr *github.Release // optional
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrVersionNotFound is returned when the specified version is not found.
|
||||
ErrVersionNotFound = errors.New("version not found")
|
||||
|
||||
// ErrDynamicTag is returned when the tag is dynamic.
|
||||
ErrDynamicTag = errors.New("dynamic tag")
|
||||
)
|
||||
|
||||
const (
|
||||
// IndexAutoUpdate is a flag to automatically update the index.
|
||||
IndexAutoUpdate = 1 << iota
|
||||
|
||||
// ToolQuietInstall is a flag to suppress output during installation.
|
||||
ToolQuietInstall
|
||||
|
||||
// LogRevertProxy is a flag to log revert proxy.
|
||||
LogRevertProxy
|
||||
|
||||
// KnownLatestVersion is a flag to use the known latest version.
|
||||
KnownLatestVersion
|
||||
)
|
||||
|
||||
// Lookup looks up a package by its path and version.
|
||||
func (p *Manager) Lookup(pkgPath, ver string, flags int) (_ *Package, err error) {
|
||||
root := p.IndexRoot()
|
||||
err = indexUpate(root, flags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pkgDir := root + "/" + pkgPath
|
||||
confFile := pkgDir + "/config.yml"
|
||||
b, err := os.ReadFile(confFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var conf config
|
||||
err = yaml.Unmarshal(b, &conf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ver == "" || ver == "latest" {
|
||||
if flags&KnownLatestVersion != 0 {
|
||||
if ver, v := conf.getKnownLatestVer(); ver != "" {
|
||||
return &Package{conf.PkgName, pkgPath, ver, v.Folder, nil, nil}, nil
|
||||
}
|
||||
}
|
||||
if conf.Template.Tag == "" {
|
||||
return nil, ErrDynamicTag
|
||||
}
|
||||
gr, e := github.GetRelease(pkgPath, "")
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
ver, err = verByTag(gr.TagName, conf.Template.Tag)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templ := conf.Template
|
||||
return &Package{conf.PkgName, pkgPath, ver, templ.Folder, &templ, gr}, nil
|
||||
}
|
||||
|
||||
if v, ok := conf.Versions[ver]; ok {
|
||||
return &Package{conf.PkgName, pkgPath, ver, v.Folder, nil, nil}, nil
|
||||
}
|
||||
if compareVer(ver, conf.Template.FromVer) < 0 {
|
||||
err = ErrVersionNotFound
|
||||
return
|
||||
}
|
||||
templ := conf.Template
|
||||
return &Package{conf.PkgName, pkgPath, ver, templ.Folder, &templ, nil}, nil
|
||||
}
|
||||
|
||||
func (p *Manager) IndexRoot() string {
|
||||
return p.cacheDir + "/index"
|
||||
}
|
||||
|
||||
func indexUpate(root string, flags int) (err error) {
|
||||
if _, err = os.Stat(root + "/.git"); os.IsNotExist(err) {
|
||||
os.RemoveAll(root)
|
||||
return indexInit(root, flags)
|
||||
}
|
||||
if flags&IndexAutoUpdate != 0 {
|
||||
quietInstall := flags&ToolQuietInstall != 0
|
||||
git, e := gitCmd.New(quietInstall, "pull", "--ff-only", "origin", "main")
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
git.Dir = root
|
||||
git.Stdout = os.Stdout
|
||||
git.Stderr = os.Stderr
|
||||
err = git.Run()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func indexInit(root string, flags int) (err error) {
|
||||
quietInstall := flags&ToolQuietInstall != 0
|
||||
git, err := gitCmd.New(quietInstall, "clone", "https://github.com/goplus/cppkg.git", root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
git.Stdout = os.Stdout
|
||||
git.Stderr = os.Stderr
|
||||
err = git.Run()
|
||||
return
|
||||
}
|
||||
|
||||
func compareVer(v1, v2 string) int {
|
||||
return semver.Compare("v"+v1, "v"+v2)
|
||||
}
|
||||
|
||||
func verByTag(tag, tagPattern string) (ver string, err error) {
|
||||
if pos := strings.IndexByte(tagPattern, '*'); pos >= 0 {
|
||||
prefix := tagPattern[:pos]
|
||||
suffix := tagPattern[pos+1:]
|
||||
if strings.HasPrefix(tag, prefix) && strings.HasSuffix(tag, suffix) {
|
||||
ver = tag[pos : len(tag)-len(suffix)]
|
||||
return
|
||||
}
|
||||
}
|
||||
return "", errors.New("tag not match: " + tag + " with " + tagPattern)
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cppkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type rtHandler func(req *http.Request) (resp *http.Response, err error)
|
||||
|
||||
func (p rtHandler) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return p(req)
|
||||
}
|
||||
|
||||
type teeReader struct {
|
||||
rc io.ReadCloser
|
||||
b bytes.Buffer
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
log *stdlog.Logger
|
||||
}
|
||||
|
||||
func (p *teeReader) Read(b []byte) (n int, err error) {
|
||||
n, err = p.rc.Read(b)
|
||||
p.b.Write(b[:n])
|
||||
return
|
||||
}
|
||||
|
||||
func (p *teeReader) Close() error {
|
||||
err := p.rc.Close()
|
||||
if log := p.log; log != nil {
|
||||
resp := *p.resp
|
||||
resp.Body = io.NopCloser(&p.b)
|
||||
var b bytes.Buffer
|
||||
p.req.Write(&b)
|
||||
resp.Write(&b)
|
||||
log.Print(b.String())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type response = httptest.ResponseRecorder
|
||||
|
||||
func newResponse() *response {
|
||||
return httptest.NewRecorder()
|
||||
}
|
||||
|
||||
type revertProxy = httptest.Server
|
||||
type rpFunc = func(mux *http.ServeMux)
|
||||
|
||||
const (
|
||||
passThrough = http.StatusNotFound
|
||||
)
|
||||
|
||||
func replyError(w http.ResponseWriter, _ error) {
|
||||
w.WriteHeader(passThrough)
|
||||
}
|
||||
|
||||
func startRevertProxy(endpoint string, f rpFunc, log *stdlog.Logger) (_ *revertProxy, err error) {
|
||||
rpURL, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var mux *http.ServeMux
|
||||
if f != nil {
|
||||
mux = http.NewServeMux()
|
||||
f(mux)
|
||||
}
|
||||
proxy := httptest.NewServer(&httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(rpURL)
|
||||
},
|
||||
Transport: rtHandler(func(req *http.Request) (resp *http.Response, err error) {
|
||||
if mux != nil {
|
||||
w := newResponse()
|
||||
mux.ServeHTTP(w, req)
|
||||
if w.Code != passThrough {
|
||||
resp = w.Result()
|
||||
}
|
||||
}
|
||||
if resp == nil {
|
||||
resp, err = http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
if err == nil && resp.Body != nil {
|
||||
resp.Body = &teeReader{
|
||||
rc: resp.Body,
|
||||
req: req,
|
||||
resp: resp,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
return
|
||||
}),
|
||||
})
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
const (
|
||||
conanCenter = "conancenter"
|
||||
conanEndpoint = "https://center2.conan.io"
|
||||
)
|
||||
|
||||
type remoteList []struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func remoteProxy(flags int, logFile string, f func() error, rpf rpFunc) (err error) {
|
||||
quietInstall := flags&ToolQuietInstall != 0
|
||||
app, err := conanCmd.Get(quietInstall)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := conanEndpoint
|
||||
cmd := exec.Command(app, "remote", "list", "-f", "json")
|
||||
if b, err := cmd.Output(); err == nil {
|
||||
var rl remoteList
|
||||
if json.Unmarshal(b, &rl) == nil {
|
||||
for _, r := range rl {
|
||||
if r.Name == conanCenter && strings.HasPrefix(r.URL, "https://") {
|
||||
endpoint = r.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
exec.Command(app, "remote", "add", "--force", conanCenter, endpoint).Run()
|
||||
}()
|
||||
|
||||
var log *stdlog.Logger
|
||||
if logFile != "" {
|
||||
f, err := os.Create(logFile)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
log = stdlog.New(f, "", stdlog.LstdFlags)
|
||||
}
|
||||
}
|
||||
rp, err := startRevertProxy(conanEndpoint, rpf, log)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rp.Close()
|
||||
|
||||
err = exec.Command(app, "remote", "add", "--force", conanCenter, rp.URL).Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return f()
|
||||
}
|
||||
Reference in New Issue
Block a user