Adds solo repository support

This commit is contained in:
sloum 2022-11-14 13:48:49 -08:00
parent 912e575463
commit d710717489
5 changed files with 230 additions and 33 deletions

View File

@ -8,7 +8,7 @@ An easy way to install, remove, and update [slope](https://git.rawtext.club/slop
slp deps [[-g]] [file] # install the dependencies of 'file'
slp docs [[-g]] [module] # open a module's readme in $PAGER
slp fetch # fetches the updated registry
slp gen # creates new module dir/skeleton
slp gen [[-s|--solo]] # creates new module dir/skeleton
slp help # print usage information
slp install [[-g]] [module...] # installs module(s)
slp installed [[-g]] # lists all installed packages
@ -23,6 +23,10 @@ slp update-all [[-g]] [module...] # updates all installed modules
The above options are more or less self-explanatory with the exception of `local`. `local` will install a module that you have on your system, but not on the slope module path. This is useful if, for example, a person has made their module available but it is not in the slp registry. In which case you can clone their repo and run `slp local ~/path/to/their-module`. Once installed in this manner the slp `remove`, `installed`, and `docs` commands will be able to operate on the module based on its folder name. `update`, however, requires the registry to know what git tag is the current/newest tag.
## VCS Choices
`slp gen` supports git repositories (the default) or [solo](https://slope.colorfield.space/solo/repos/solo) repositories. To create the new module folder as a solo repository instead of a git repository simply pass the `-s` or `--solo`. For example: `slp gen -s` or `slp gen --solo`. If you opt to use solo repositories be sure that the repository link in your `module.json` file is a link to the `[reponame].tar.gz` file that soloweb creates when pushing a repository. Linking to the index page of the soloweb view of the repository will not do the job. If you are not sure where the correct file is, but have pushed your repository to a remote using `solo push [credentials]`, you can visit the web page and right click on the `download` link in the navigation menu and choose `copy link`. That will add the correct link to your clipboard, and it can then be added to your `module.json` file as needed.
## Global installs
Operations that accept a -g flag will attempt to install a module systemwide (this may require root access). A --global flag may be passed in lieu of a -g flag if desired for clarity.

10
main.go
View File

@ -17,7 +17,7 @@ const (
helptext string = `
slp deps [[-g]] [file] # install the dependencies of 'file'
slp fetch # fetches the updated registry
slp gen # creates new module dir/skeleton
slp gen [[-s|--solo]] # creates new module dir/skeleton
slp help # print usage information
slp install [[-g]] [module...] # installs module(s)
slp installed [[-g]] # lists all installed packages
@ -32,6 +32,8 @@ slp version # print the current slp version
Operations that accept a -g flag will attempt to install a module systemwide (this may require root access). A --global flag may be passed in lieu of a -g flag if desired for clarity.
slp gen will automatically init a git repository in the created folder. The -s/--solo flag will change that behavior to create a solo repository instead.
The install location for global modules is: /usr/local/lib/slope/modules
Globals modules must be dealt with separately from local modules and cannot be combined in a single command.`
@ -108,7 +110,11 @@ MainSwitch:
os.Exit(1)
}
case "gen":
err := operators.Generate()
solo := false
if len(arg) > 2 && arg[2] == "-s" || arg[2] == "--solo" {
solo = true
}
err := operators.Generate(solo)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)

View File

@ -52,7 +52,7 @@ func (g genOpts) String() string {
// Generate generates a package
// by cloning the package template repo
func Generate() error {
func Generate(solo bool) error {
opts, err := promptOptions()
if err != nil {
return err
@ -70,9 +70,27 @@ func Generate() error {
dir := filepath.Join(pwd, opts.Title)
_, err = git.PlainInit(dir, false)
if err != nil {
return err
if solo {
err = os.MkdirAll(filepath.Join(dir, ".solo", "tree", "master"), 0755)
if err != nil {
return err
}
err = os.MkdirAll(filepath.Join(dir, ".solo", "tags"), 0755)
if err != nil {
return err
}
f, err := os.Create(filepath.Join(dir, ".solo", "solo.conf"))
if err != nil {
return err
}
defer f.Close()
host, _ := os.Hostname()
f.WriteString(fmt.Sprintf(`(("heads" ()) ("branch" "master") ("tags" ()) ("description" "%s") ("author" "%s@%s") ("version" (0 3 0)) ("remote" #f) ("floating?" #f))`, opts.Description, opts.Author, host))
} else {
_, err = git.PlainInit(dir, false)
if err != nil {
return err
}
}
jf, err := os.Create(filepath.Join(dir, "module.json"))
@ -92,7 +110,7 @@ func Generate() error {
sf.WriteString(fmt.Sprintf(";;; Author: %s\n", opts.Author))
sf.WriteString(fmt.Sprintf(";;; Version: %s\n", opts.Version))
sf.WriteString(";;;\n\n; display \"Hello, World!\"\n")
sf.WriteString(fmt.Sprintf("(define %s-hello-world (lambda ()\n", opts.Title))
sf.WriteString("(define hello-world (lambda ()\n")
sf.WriteString(fmt.Sprintf(" (display \"'Hello, world!' from %s\\n\")))\n\n; vim: ts=2 sw=2 expandtab ft=slope\n", opts.Title))
rm, err := os.Create(filepath.Join(dir, "README.md"))

View File

@ -51,33 +51,39 @@ func Install(pkg string, global bool) error {
for k, _ := range depList {
fmt.Printf(" \033[2m├\033[0m staging %q\n", k)
p := packages[k]
repository, err := git.PlainClone(filepath.Join(stagingDir, p.Title), false, &git.CloneOptions{
URL: p.Repository,
})
if err != nil {
failedInstall = append(failedInstall, err.Error())
continue
}
if p.Tag != "" {
wt, err := repository.Worktree()
if err != nil {
if strings.HasSuffix(p.Repository, "tar.gz") {
if err := InstallFromSolo(p.Title, p.Repository, p.Tag, stagingDir); err != nil {
failedInstall = append(failedInstall, err.Error())
continue
}
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewTagReferenceName(p.Tag),
} else {
repository, err := git.PlainClone(filepath.Join(stagingDir, p.Title), false, &git.CloneOptions{
URL: p.Repository,
})
if err != nil {
failedInstall = append(failedInstall, err.Error())
continue
}
}
_, err = os.Stat(filepath.Join(stagingDir, pkg, "main.slo"))
if err != nil && os.IsNotExist(err) {
failedInstall = append(failedInstall, fmt.Sprintf(" \033[2m└\033[0m \033[91mError:\033[0m package %q does not contain a valid 'main.slo' file", pkg))
if p.Tag != "" {
wt, err := repository.Worktree()
if err != nil {
failedInstall = append(failedInstall, err.Error())
continue
}
err = wt.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewTagReferenceName(p.Tag),
})
if err != nil {
failedInstall = append(failedInstall, err.Error())
continue
}
}
_, err = os.Stat(filepath.Join(stagingDir, pkg, "main.slo"))
if err != nil && os.IsNotExist(err) {
failedInstall = append(failedInstall, fmt.Sprintf(" \033[2m└\033[0m \033[91mError:\033[0m package %q does not contain a valid 'main.slo' file", pkg))
}
}
}
if len(failedInstall) > 0 {
@ -88,13 +94,19 @@ func Install(pkg string, global bool) error {
return fmt.Errorf(strings.Join(failedInstall, "\n"))
}
fmt.Println(" \033[2m├\033[0m moving staged items to modules folder")
cmd := exec.Command("sh", "-c", fmt.Sprintf("mv %s %s", filepath.Join(stagingDir, "*"), modDir))
err = cmd.Run()
if err != nil {
if global {
return fmt.Errorf("Unable to write modules to final location. Do you have access?")
globPattern := filepath.Join(stagingDir, "*")
glob, _ := filepath.Glob(globPattern)
if len(glob) > 0 {
cmd := exec.Command("sh", "-c", fmt.Sprintf("mv %s %s", globPattern, modDir))
cmd.Stdout = nil
cmd.Stderr = nil
err = cmd.Run()
if err != nil {
if global {
return fmt.Errorf("Unable to write modules to final location. Do you have access?")
}
return err
}
return err
}
return nil
}

157
operators/solo.go Normal file
View File

@ -0,0 +1,157 @@
package operators
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
func inStringSlice(slice []string, value string) bool {
for i := range slice {
if slice[i] == value {
return true
}
}
return false
}
func InstallFromSolo(repoName, repo, tag, targetDir string) error {
// Get the repo
tarGzName, err := getSoloTarGz(repo)
if err != nil {
return err
}
defer os.Remove(tarGzName)
// Un-gzip the repo
tarName, err := unGzip(tarGzName, filepath.Dir(tarGzName))
if err != nil {
return err
}
defer os.Remove(tarName)
repoFolder, err := unTar(tarName, filepath.Dir(tarName))
if err != nil {
// return err
return fmt.Errorf("3")
}
defer os.RemoveAll(repoFolder)
tagFile, err := os.ReadFile(filepath.Join(repoFolder, ".solo", "tags", tag))
if err != nil {
return fmt.Errorf("The tag %q does not exist", tag)
}
re := regexp.MustCompile(`\("([^"\n]+)"\s+"([^"\n"]+)"[^\)]*\)`)
matches := re.FindAllStringSubmatch(string(tagFile), 1)
if len(matches) == 0 || len(matches[0]) < 3 {
return fmt.Errorf("The tag %q is corrupted and cannot be used", tag)
}
branch := string(matches[0][1])
snap := string(matches[0][2])
if branch == "" || snap == "" {
return fmt.Errorf("The tag %q contains invalid references and cannot be used", tag)
}
folderRoot := filepath.Join(repoFolder, ".solo", "tree", branch, snap, "data")
files, _ := filepath.Glob(filepath.Join(folderRoot, "*"))
if len(files) == 0 {
return fmt.Errorf("Invalid repository")
}
if !inStringSlice(files, filepath.Join(folderRoot, "main.slo")) || !inStringSlice(files, filepath.Join(folderRoot, "module.json")) {
return fmt.Errorf("Invalid repository structure")
}
return os.Rename(folderRoot, filepath.Join(targetDir, repoName))
}
func getSoloTarGz(u string) (string, error) {
if !strings.HasSuffix(u, ".tar.gz") {
return "", fmt.Errorf("Invalid repository URL")
}
resp, err := http.Get(u)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
f, err := os.CreateTemp("", path.Base(u))
if err != nil {
return "", err
}
defer f.Close()
f.Write(body)
return f.Name(), nil
}
func unTar(tarPath, destPath string) (string, error) {
reader, err := os.Open(tarPath)
if err != nil {
return "", err
}
defer reader.Close()
tarReader := tar.NewReader(reader)
root := ""
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return "", err
}
path := filepath.Join(destPath, header.Name)
info := header.FileInfo()
if info.IsDir() {
if err = os.MkdirAll(path, info.Mode()); err != nil {
return "", err
}
if root == "" {
root = path
}
continue
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
if err != nil {
return "", err
}
defer file.Close()
_, err = io.Copy(file, tarReader)
if err != nil {
return "", err
}
}
return root, nil
}
func unGzip(source, target string) (string, error) {
reader, err := os.Open(source)
if err != nil {
return "", err
}
defer reader.Close()
archive, err := gzip.NewReader(reader)
if err != nil {
return "", err
}
defer archive.Close()
splitInd := strings.Index(source, ".gz")
target = filepath.Join(target, filepath.Base(source)[:splitInd])
writer, err := os.Create(target)
if err != nil {
return "", err
}
defer writer.Close()
_, err = io.Copy(writer, archive)
return target, err
}