Sloum's Compiled Build Manager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

694 lines
21 KiB

package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"sort"
"strings"
git "github.com/gogits/git-module"
)
const VERSION string = "0.0.5"
const SCBM_FOLDER string = "/var/local/scbm"
const MANIFEST string = "/var/local/scbm/_data/manifest.txt"
var bool2int = map[bool]int{false: 0, true: 1}
type record struct {
name string
url string
command string
current string
prev string
desc string
}
func (rec *record) String() string {
return fmt.Sprintf(
"%s\036%s\036%s\036%s\036%s\036%s",
rec.name,
rec.url,
rec.command,
rec.current,
rec.prev,
rec.desc)
}
func (rec *record) saveRecord() error {
fp := filepath.Join(SCBM_FOLDER, "_data", fmt.Sprintf("%s.scbm", rec.name))
err := ioutil.WriteFile(fp, []byte(rec.String()), 0644)
return err
}
func getRecord(name string) (record, error) {
fp := filepath.Join(SCBM_FOLDER, "_data", fmt.Sprintf("%s.scbm", name))
if _, err := os.Stat(SCBM_FOLDER); os.IsNotExist(err) {
return record{}, err
}
f, err := ioutil.ReadFile(fp)
if err != nil {
return record{}, err
}
fields := strings.Split(string(f), "\036")
if len(fields) != 6 {
return record{}, fmt.Errorf("Incorrect number of fields in scbm file")
}
rec := record{}
rec.name = fields[0]
if rec.name != name {
return rec, fmt.Errorf("Invalid scbm format, name does not match file")
}
rec.url = fields[1]
rec.command = fields[2]
rec.current = fields[3]
rec.prev = fields[4]
rec.desc = fields[5]
return rec, nil
}
type manifest struct {
m map[string]bool
}
func buildManifest() (manifest, error) {
m := manifest{map[string]bool{}}
err := m.load()
return m, err
}
func (m *manifest) load() error {
f, err := ioutil.ReadFile(MANIFEST)
if err != nil {
return err
}
lines := strings.Split(string(f), "\n")
for _, l := range lines {
var k string
var v int
_, err := fmt.Sscanf(l, "%s\t%d", &k, &v)
if err != nil {
continue
}
if v == 0 {
m.m[k] = false
} else if v == 1 {
m.m[k] = true
}
}
return nil
}
func (m *manifest) freeze(name string) error {
if _, ok := m.m[name]; !ok {
return fmt.Errorf("No program nammed %s is present in the manifest", name)
}
m.m[name] = false
return nil
}
func (m *manifest) thaw(name string) error {
if _, ok := m.m[name]; !ok {
return fmt.Errorf("No program nammed %s is present in the manifest", name)
}
m.m[name] = true
return nil
}
func (m *manifest) trash(name string) error {
if _, ok := m.m[name]; !ok {
return fmt.Errorf("No program nammed %s is present in the manifest", name)
} else {
delete(m.m, name)
}
err := m.save()
if err != nil {
return err
}
fp := filepath.Join(SCBM_FOLDER, name)
err = os.RemoveAll(fp)
if err != nil {
return err
}
fp = filepath.Join(SCBM_FOLDER, fmt.Sprintf("_data/%s.scbm", name))
err = os.Remove(fp)
return err
}
func (m *manifest) save() error {
var buf strings.Builder
for k, v := range m.m {
buf.WriteString(fmt.Sprintf("%s\t%d\n", k, bool2int[v]))
}
out := buf.String()
if len(out) > 1 {
out = out[:len(out)-1]
}
err := ioutil.WriteFile(MANIFEST, []byte(out), 0644)
if err != nil {
return err
}
return nil
}
///////////////////////////////
// Startup & helpers
//////////////////////////////
// validateScbmFolder checks to make sure the necessary data storage folder(s) exist.
// It will not create any folder that do no exist, but will print the error and a user
// readable message and then exit with an exit code of 1.
func validateScbmFolder() {
if _, err := os.Stat(SCBM_FOLDER); os.IsNotExist(err) {
fmt.Println("scbm storage area error:")
fmt.Println(strings.TrimSpace(err.Error()))
os.Exit(1)
}
}
// parseArgs is the main entry point into scbm and will run the appropriate functions
// based on the arguments passed in at run time.
func parseArgs() {
printHeader()
a := os.Args
if len(a) == 1 {
displayUsage()
}
switch a[1] {
case "help":
if len(a) == 3 {
help(a[2])
} else if len(a) == 2 {
help("general")
} else {
fmt.Fprintf(os.Stderr, "Too many items received.\nExpected: scbm help [command]\nGot: scbm help %s\n", strings.Join(a[3:], " "))
os.Exit(1)
}
case "get":
get()
case "install":
if len(a) < 3 {
errorExit("Incorrect number of arguments to install. Expected 1+ arguments: program-name ...", nil)
}
install(a[2:])
case "list":
list()
case "freeze":
freezeCommand := flag.NewFlagSet("freeze", flag.ExitOnError)
trashFlag := freezeCommand.Bool("trash", false, "Will delete a repository rather than just mark it incative")
freezeCommand.Parse(os.Args[2:])
freeze(*trashFlag, freezeCommand.Args())
case "thaw":
if len(a) < 3 {
errorExit("Too few arguments to view. Expected 1 argument: program-name.", nil)
}
thaw(a[2:])
case "revert":
revertCommand := flag.NewFlagSet("revert", flag.ExitOnError)
swapFlag := revertCommand.Bool("swap", false, "When set, the previous commit ID will be made current, like in a standard revert; but the current will become the previous, so the revert can be reverted.")
if len(a) < 3 {
errorExit("Incorrect number of arguments to revert. Expected 1 argument and optionally one flag", nil)
}
revertCommand.Parse(os.Args[2:])
prog := revertCommand.Args()
if len(prog) > 1 {
errorExit("Incorrect number of arguments to revert. Expected 1 argument and optionally one flag", nil)
}
revert(prog[0], *swapFlag)
case "set":
if len(a) != 4 {
errorExit("Too few arguments to set. Expected 2 arguments: program-name and command.", nil)
}
set(a[2], a[3])
case "update":
if len(a) > 2 {
update(a[2:])
} else {
update(make([]string, 0))
}
case "view":
if len(a) < 3 {
errorExit("Too few arguments to view. Expected 1 argument: name.", nil)
}
view(a[2:])
default:
fmt.Fprintf(os.Stderr, "Unknown command %q\n", a[1])
displayUsage()
}
}
// displayUsage is called when an invalid command structure is passed
// into scbm. It prints the correct structure to os.Stderr and exits
// with an exit code of 1.
func displayUsage() {
fmt.Fprintln(os.Stderr, "scbm command [opts ...]")
os.Exit(1)
}
func errorExit(msg string, err error) {
fmt.Fprintln(os.Stderr, msg)
if err != nil {
fmt.Fprintln(os.Stderr, strings.TrimSpace(err.Error()))
}
os.Exit(1)
}
func printHeader() {
fmt.Printf("\033[1mscbm - v%s\n", VERSION)
}
///////////////////////////////
// Command entry points
//////////////////////////////
// get takes in arguments from the args array in
// the form of a URL and a name. get will attempt
// to clone the repo at the url to an appropriate
// folder, optionally named by name.
func get() {
a := os.Args
if len(a) != 3 && len(a) != 4 {
numItems := "Too many"
if len(a) == 2 {
numItems = "Too few"
}
errorExit(fmt.Sprintf("%s arguments received for \"get\"", numItems), nil)
}
url := a[2]
name := ""
if len(a) == 4 {
name = a[3]
} else {
base := path.Base(url)
if strings.HasSuffix(base, ".git") {
name = base[:len(base)-4]
} else {
name = base
}
}
fp := filepath.Join(SCBM_FOLDER, name)
// Clone the repo
fmt.Printf("Cloning: %s from %s\n", name, url)
err := git.Clone(url, fp, git.CloneRepoOptions{})
if err != nil {
errorExit(fmt.Sprintf("Error cloning %s:", name), err)
}
// Get the most recent commit id (the one being used),
// currently only master branch is supported
r, err := git.OpenRepository(fp)
if err != nil {
errorExit(fmt.Sprintf("Error accessing %s repository @ %s:", name, fp), err)
}
commit, err := r.GetBranchCommitID("master")
if err != nil {
errorExit(fmt.Sprintf("Error getting current commit id:"), err)
}
rec := record{
name,
url,
"",
commit,
"",
"No description provided"}
// Get the description
fp = filepath.Join(fp, ".git/description")
b, err := ioutil.ReadFile(fp)
if err == nil {
s := string(b)
if !strings.HasPrefix(s, "Unnamed repository") {
rec.desc = strings.Replace(s, "\n", " ", -1)
}
}
// Update the manifest
fmt.Printf("Adding %s to program manifest\n", name)
f, err := os.OpenFile(MANIFEST, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
errorExit("Error opening manifest:", err)
}
if _, err = f.WriteString(fmt.Sprintf("\n%s\t1", name)); err != nil {
errorExit("Error writing to manifest:", err)
}
f.Close()
fmt.Printf("Generating configuration file for %s\n", name)
fp = filepath.Join(SCBM_FOLDER, fmt.Sprintf("_data/%s.scbm", name))
f, err = os.OpenFile(fp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
errorExit("Error opening configuration file:", err)
}
if _, err = f.WriteString(rec.String()); err != nil {
errorExit("Error writing configuration file:", err)
}
f.Close()
fmt.Println("Done.")
}
// help functions as a router to print out help information
// for each command, offering detailed explanations of the
// command and any available flags.
func help(c string) {
c = strings.ToLower(c)
switch c {
case "general":
fmt.Println("Format:\n\tscbm \033[3mcommand\033[0m [option...] [argument...]")
fmt.Println("\nscbm is a compiled build manager for git repositories. The basic idea is to be able to manage items that are built from source and provide an easy mechanism to update/upgrade these programs. Typing `scbm help` and then a command will yield further help with that command. Available commands are:\n\n\tfreeze\n\tget\n\tinstall\n\tlist\n\trevert\n\tset\n\tupdate")
case "freeze":
fmt.Println("\033[1;4mfreeze\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm freeze [-trash] \033[3mname...\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill freeze the given program(s) (prevent from receiving further updates). \033[1mDOES NOT\033[0m delete any files or uninstall any programs. If the `-trash` flag is passed the repo(s) will be removed from the scbm folder; this is not recommended unless you have properly uninstalled the program beforehand as `-trash` only removes the program from management by scbm. It does not remove or alter any files outside of the scbm folder. While a frozen program will not receive updates, it can still be installed with the `install` command or have its installation string updated with the `set` command.")
case "thaw":
fmt.Println("\033[1;4mthaw\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm thaw \033[3mname...\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill reactivate the given frozen program(s), allowing it to receive updates with the `update` command.")
case "list":
fmt.Println("\033[1;4mlist\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm list")
fmt.Println("\033[1mAction:\033[0m\nWill output a table displaying all of the programs under scbm management showing the program name, the discription, and the program's freeze status.")
case "get":
fmt.Println("\033[1;4mget\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm get \033[3mclone-url\033[0m [name]")
fmt.Println("\033[1mAction:\033[0m\nWill attempt to clone the repo at the given url into the scbm staging folder and add the program to the scbm databse. The name field will default to the new repo name, but can be replaced by a name passed in as the final argument to the get command. The current HEAD of the master branch will be set as the current commit.")
case "update":
fmt.Println("\033[1;4mupdate\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm update \033[3m[name...]\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill update the given program(s). Absent a program list scbm will try to update all of the programs it manages. Updating consists of pulling each git repository and updating the current hash for the program and moving the previous current to be set as `previous`, allowing a revert if desired.")
case "install":
fmt.Println("\033[1;4minstall\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm install \033[3mname...\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill run the install command string for the given program name(s). If a program has an empty command string, no action will be taken. User's should watch the output for any errors that their install command strings may report so that they can be fixed.")
case "set":
fmt.Println("\033[1;4mset\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm set \033[3mname command-string\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill set the install command for the given program to the given command-string.")
case "revert":
fmt.Println("\033[1;4mrevert\033[0m")
fmt.Println("\033[1mFormat:\033[0m\n\tscbm revert [-swap] \033[3mname\033[0m")
fmt.Println("\033[1mAction:\033[0m\nWill set the branch of the given program to the previously stored commit hash, if one exists. If the swap flag is passed, the current commit hash and the previous will swap places. If a program is successfully reverted, it will automatically be frozen and installed. This is useful to return the program to a known working state.")
default:
fmt.Printf("No help exists for %q...\n", c)
}
}
// list loads the manifest file into memory and will print out
// each program name and description, so long as the program is
// active (set to 1/has not been forzen)
func list() {
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
keys := make([]string, len(m.m), len(m.m))
i := 0
for k, _ := range m.m {
keys[i] = k
i++
}
sort.Strings(keys)
fmt.Printf("\033[1;4m%-15s\033[0m \033[1;4m%-8s\033[0m \033[1;4m%-35s\033[0m\n", "Name", "Frozen", "Description")
for _, k := range keys {
r, e := getRecord(k)
if e != nil {
continue
}
fmt.Printf("\033[1m%-15s\033[0m %-8t %s\n", r.name, !m.m[k], r.desc)
}
fmt.Println("")
}
// freeze sets a given program to inactive (0 in the manifest). When
// in this state it will not be included in lists, updates, etc. If
// the trash flag is passed it will be hard deleted from the system,
// including the repo itself, references in the manifest, and scbm
// file(s).
func freeze(trash bool, progs []string) {
fmt.Println("Loading manifest data")
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
for _, prog := range progs {
if trash {
fmt.Printf("Trashing %s\n", prog)
err = m.trash(prog)
if err != nil {
fmt.Printf(" \\_Error trashing %s: %s ]\n", prog, strings.TrimSpace(err.Error()))
fmt.Printf(" \033[1m%s may be left in an invalid state\033[0m", prog)
}
} else {
fmt.Printf("Freezing %s\n", prog)
err = m.freeze(prog)
if err != nil {
fmt.Printf(" \\_Error freezing%s: %s ]\n", prog, strings.TrimSpace(err.Error()))
}
}
}
err = m.save()
if err != nil {
errorExit("Error saving manifest:", err)
}
fmt.Println("Done.")
}
func thaw(progs []string) {
fmt.Println("Loading manifest data")
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
for _, prog := range progs {
fmt.Printf("Thawing %s\n", prog)
err = m.thaw(prog)
if err != nil {
fmt.Printf(" \\_Error thawing %s: %s ]\n", prog, strings.TrimSpace(err.Error()))
}
}
err = m.save()
if err != nil {
errorExit("Error saving manifest:", err)
}
fmt.Println("Done.")
}
// update pulls from `origin master` for each active repository in
// the program list provided, or absent a list pulls `origin master`
// for each active program in the manifest.
func update(progs []string) {
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
if len(progs) == 0 {
progs = make([]string, len(m.m), len(m.m))
i := 0
for k, v := range m.m {
if v {
progs[i] = k
}
i++
}
}
for _, prog := range progs {
if !m.m[prog] {
fmt.Printf("Skipping \033[4m%s\033[0m, it is currently frozen\n", prog)
continue
}
r, e := getRecord(prog)
if e != nil {
continue
}
fmt.Printf("\033[1mPulling updates for \033[1;4m%s\033[0m\n", prog)
fp := filepath.Join(SCBM_FOLDER, prog)
opts := git.PullRemoteOptions{false, false, "origin", "master", -1}
err := git.Pull(fp, opts)
if err != nil {
fmt.Printf(" \\_[ Error pulling %q: %s ]\n", prog, strings.TrimSpace(err.Error()))
continue
}
// Get the most recent commit id (the one being used),
// currently only master branch is supported
repo, err := git.OpenRepository(fp)
if err != nil {
errorExit(fmt.Sprintf("Error accessing %s repository @ %s:", prog, fp), err)
}
commit, err := repo.GetBranchCommitID("master")
if err != nil {
errorExit(fmt.Sprintf("Error getting current commit id:"), err)
}
if commit != r.current {
r.prev = r.current
r.current = commit
r.saveRecord()
fmt.Printf("%s HEAD now set to %s\n", prog, commit)
} else {
fmt.Printf(" \\_No updates for %s\n", prog)
continue
}
// TODO remove the install step and add the program name
// to an upgradable.txt file. This should always be done
// as an append. When upgrade runs it will remove all of
// the ones that successfully installed.
install([]string{prog})
}
}
// TODO upgrade will handle
func upgrade() {
}
// revert moves a program's HEAD back to the previously used commit,
// if one exists. It moves the current commit ID to that commit and
// runs the install script. It then freezes the program so-as to stay
// on that commit.
func revert(prog string, swap bool) {
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
fmt.Printf("Reverting %s...\n", prog)
if val, ok := m.m[prog]; ok && !val {
errorExit(fmt.Sprintf(" \\_%s is currently frozen. To revert or make other changes please thaw it.", prog), nil)
} else if !ok {
errorExit(fmt.Sprintf(" \\_%s does not exist\n", prog), nil)
}
r, e := getRecord(prog)
if e != nil {
errorExit(fmt.Sprintf("Unable to retrieve record for %s", prog), nil)
}
if strings.TrimSpace(r.prev) == "" {
errorExit(fmt.Sprintf(" \\_%s does not have a previous commit to revert to", prog), nil)
}
newCurrent := r.prev
fp := filepath.Join(SCBM_FOLDER, prog)
fmt.Printf("Reverting %s to previous commit: %s", prog, newCurrent)
err = git.ResetHEAD(fp, true, newCurrent)
if err != nil {
errorExit(fmt.Sprintf(" \\_unable to revert %s to %s", prog, newCurrent), err)
}
if swap {
fmt.Println("Swapping current and previous commit")
r.prev = r.current
} else {
r.prev = ""
}
r.current = newCurrent
fmt.Printf("Updating records for %s", prog)
err = r.saveRecord()
if err != nil {
fmt.Printf(" \\_\033[7mError saving record for %s:\033[0m\n", prog)
fmt.Println(err)
fmt.Println("\033[7mUndoing revert...\033[0m")
update([]string{prog})
return
}
freeze(false, []string{prog})
install([]string{prog})
}
func install(progs []string) {
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
for _, prog := range progs {
fmt.Printf("\033[1mInstalling\033[0m %s...\n", prog)
if _, ok := m.m[prog]; !ok {
fmt.Printf(" \\_Program %s does not exist in the scbm manifest\n \\_skipping...\n", prog)
continue
}
rec, err := getRecord(prog)
if err != nil {
fmt.Printf(" \\_Unable to retrieve record for %s: ", prog)
fmt.Print(err.Error())
fmt.Println(" \\_Skipping...")
continue
}
if strings.TrimSpace(rec.command) == "" {
fmt.Printf(" \\_Empty command string for %s... aborting install.\n", prog)
continue
}
fmt.Printf("%s => %s\n", prog, rec.command)
cmd := exec.Command("sh", "-c", fmt.Sprintf("%s", rec.command))
cmd.Dir = filepath.Join(SCBM_FOLDER, prog)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
}
}
// set sets the command that is to be run for installation of a
// program, eventually a flag should get added to this to allow
// for pointing to a file rather than entering a raw shell command.
func set(prog, com string) {
m, err := buildManifest()
if err != nil {
errorExit("Error loading manifest:", err)
}
if _, ok := m.m[prog]; !ok {
errorExit(fmt.Sprintf("Program %s does not exist in the scbm manifest", prog), nil)
}
rec, err := getRecord(prog)
if err != nil {
errorExit(fmt.Sprintf("Unable to retrieve record for %s", prog), err)
}
fmt.Printf("Setting %s's command to: %q\n", prog, com)
rec.command = com
err = rec.saveRecord()
if err != nil {
errorExit(fmt.Sprintf(" \\_\033[7mError saving record for %s:\033[0m", prog), err)
}
fmt.Println("Done.")
}
// view outputs the details of an individual program record to stdout
func view(tail []string) {
if len(tail) != 1 {
errorExit("Too many positional arguments were received by view", nil)
}
rec, err := getRecord(tail[0])
if err != nil {
errorExit(fmt.Sprintf("Unable to retrieve record for %s", tail[0]), err)
}
fmt.Printf("\033[1;4m%s\033[0m\n\n", tail[0])
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Name", rec.name)
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Remote Url", rec.url)
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Build Command", rec.command)
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Current Commit", rec.current)
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Prev Commit", rec.prev)
fmt.Printf("\033[1m%-15s\033[0m: %s\n", "Description", rec.desc)
}
func main() {
validateScbmFolder()
parseArgs()
}