├── .gitignore ├── README.md ├── main.go ├── pkgbuild.go └── service.go /.gitignore: -------------------------------------------------------------------------------- 1 | /go-makepkg 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-makepkg 2 | 3 | Tool for fast packaging Golang programs under the archlinux. 4 | 5 | It will automatically generate appropriate PKGBUILD and systemd.service files. 6 | 7 | ## How to use 8 | 9 | 0. `go get github.com/seletskiy/go-makepkg`; 10 | 1. `mkdir some-directory`; 11 | 2. `cd some-directory`; 12 | 3. `mkdir -p etc/mycoolprog/`; 13 | 4. Copy any other required files for you program, like config files: 14 | `cp /example.conf etc/mycoolprog/main.conf`; 15 | 5. Omit `-s` flag if you do not want service file: 16 | `go-makepkg -sB "my description" git://url-to-prog/repo.git **/*`; 17 | 6. Package is ready for install and located at `build/.tar.xz`; 18 | 19 | ## Typical invocation 20 | 21 | ``` 22 | go-makepkg -gsB "my cool package" git://github.com/seletskiy/go-makepkg * 23 | ``` 24 | 25 | Will generate .gitignore, PKGBUILD and .service file for specified repo (e.g. 26 | go-makepkg) and include all files under current directory to the package. 27 | 28 | If you do not want to build package automatically, omit `-B` flag. 29 | 30 | See `go-makepkg -h` for more info. 31 | 32 | `go-makepkg` by itself can be packaged using itself: 33 | `go-makepkg -B "go-makepkg tool" git://github.com/seletskiy/go-makepkg.git` 34 | 35 | ### Pass package version into golang code 36 | 37 | As you know, you can change global variables of your Golang program in the 38 | compile time by using `go build` options, like in the following example you can 39 | change variable `packageVersion` using `ldflags`. 40 | 41 | ```go 42 | package main 43 | 44 | import "fmt" 45 | 46 | var blahme = "autogenerated" 47 | 48 | func main() { 49 | fmt.Println(blahme) 50 | } 51 | ``` 52 | 53 | Value of the `blahme` can be changed: 54 | ``` 55 | go build -ldflags="-X main.blahme=testvalue" -o test . 56 | ``` 57 | 58 | Run `./test` and you will see `testvalue` instead of hardcoded `autogenerated` 59 | string. 60 | 61 | It's very useful opportunity, for escaping the hell of versioning your 62 | software and shift this work to the PKGBUILD. 63 | 64 | `go-makepkg` can do it for you, all you need is to specify a variable name 65 | which holds version number using `-p ` flag: 66 | 67 | ``` 68 | go-makepkg -p version "go-makepkg tool" git://github.com/seletskiy/go-makepkg.git 69 | ``` 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/url" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/docopt/docopt-go" 17 | ) 18 | 19 | var version = "3.1" 20 | var usage = `PKGBUILD generator for Golang programs. 21 | 22 | Will create PKGBUILD which can be used for building package from specified 23 | repo, including optinal additional files to the package. 24 | 25 | Tool also capable of creating simple systemd.service file for starting/stoping 26 | daemon and creating .gitignore file in the build directory. 27 | 28 | If you want to include additional files to the package, make sure they are 29 | stored by the path they will be placed in the system after package 30 | installation. 31 | E.g., if you want to include config to the package, place it into 32 | 'etc/somename/config.conf' directory. 33 | 34 | 'go-makepkg' will store generated build, service and additional files in the 35 | 'build' directory (by default). 36 | 37 | Trivial run is (all files in the directory except generated will be included 38 | into the package): 39 | go-makepkg "my cool package" git://my-repo-url **/* -B 40 | 41 | Note: if you want to create package for the project, which uses sub-directories 42 | for binaries and go-gettable with suffix '...', you should specify that suffix 43 | to repo URL as well, like: 44 | go-makepkg "gb tool" git://github.com/constabulary/gb/... -B 45 | 46 | Usage: 47 | go-makepkg [options] [...] 48 | go-makepkg -h | --help 49 | go-makepkg -v | --version 50 | 51 | Options: 52 | -v --version Show version. 53 | -h --help Show this help. 54 | -s Create service file and include it to the package. 55 | -g Create .gitignore file. 56 | -B Run 'makepkg' after creating PKGBUILD. 57 | -c Clean up leftover files and folders. 58 | -n Use specified package name instead of automatically generated 59 | from URL. 60 | -l License to use [default: GPL]. 61 | -r Specify package release number [default: 1]. 62 | -d Directory to place PKGBUILD [default: build]. 63 | -o File to write PKGBUILD [default: PKGBUILD]. 64 | -m Specify maintainer$MAINTAINER. 65 | -p Pass pkgver to specified global variable using ldflags. 66 | -D Comma-separated list of runtime package dependencies (depends). 67 | -M Comma-separated list of make package dependencies (makedepends). 68 | ` 69 | 70 | type pkgFile struct { 71 | Path string 72 | Name string 73 | Hash string 74 | } 75 | 76 | type pkgData struct { 77 | Maintainer string 78 | PkgName string 79 | PkgRel string 80 | PkgDesc string 81 | ProgramName string 82 | RepoURL string 83 | License string 84 | Files []pkgFile 85 | Dependencies []string 86 | MakeDependencies []string 87 | Backup []string 88 | IsWildcardBuild bool 89 | VersionVarName string 90 | } 91 | 92 | type serviceData struct { 93 | Description string 94 | ExecName string 95 | } 96 | 97 | func parseCommaList(v interface{}) []string { 98 | if v == nil { 99 | return []string{} 100 | } 101 | return strings.Split(v.(string), ",") 102 | } 103 | 104 | func main() { 105 | args, err := docopt.Parse( 106 | replaceUsageDefaults(usage), 107 | nil, true, "go-makepkg "+version, false, true, 108 | ) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | var ( 114 | description = args[``].(string) 115 | rawRepoURL = args[``].(string) 116 | fileList = args[``].([]string) 117 | license = args[`-l`].(string) 118 | packageRelease = args[`-r`].(string) 119 | dirName = args[`-d`].(string) 120 | outputName = args[`-o`].(string) 121 | doRunBuild = args[`-B`].(bool) 122 | doCleanUp = args[`-c`].(bool) 123 | doCreateService = args[`-s`].(bool) 124 | doCreateGitignore = args[`-g`].(bool) 125 | maintainer, _ = args[`-m`].(string) 126 | versionVarName, _ = args[`-p`].(string) 127 | dependencies = parseCommaList(args[`-D`]) 128 | makeDependencies = parseCommaList(args[`-M`]) 129 | ) 130 | 131 | safeRepoURL, isWildcardBuild := trimWildcardFromRepoURL(rawRepoURL) 132 | 133 | repoURL, err := url.Parse(safeRepoURL) 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | 138 | if repoURL.Scheme == "ssh" || repoURL.Scheme == "ssh+git" { 139 | safeRepoURL = strings.Replace( 140 | safeRepoURL, repoURL.Scheme, "git+ssh", -1, 141 | ) 142 | } 143 | 144 | // handle git@github.com: 145 | if strings.Contains(repoURL.Host, ":") { 146 | safeRepoURL = strings.Replace( 147 | safeRepoURL, 148 | repoURL.Host, 149 | strings.Replace(repoURL.Host, ":", "/", -1), 150 | -1, 151 | ) 152 | } 153 | 154 | packageName := getPackageNameFromRepoURL(safeRepoURL) 155 | if args[`-n`] != nil { 156 | packageName = args[`-n`].(string) 157 | } 158 | 159 | err = createOutputDir(dirName) 160 | if err != nil { 161 | log.Fatal(err) 162 | } 163 | 164 | files, err := prepareFileList(fileList, dirName) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | err = copyLocalFiles(files, dirName) 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | 174 | backup := createBackupList(files) 175 | 176 | if doCreateService { 177 | serviceName := fmt.Sprintf("%s.service", packageName) 178 | output, err := os.Create(filepath.Join( 179 | dirName, 180 | serviceName, 181 | )) 182 | 183 | if err != nil { 184 | log.Fatal(err) 185 | } 186 | 187 | err = createServiceFile(output, serviceData{ 188 | Description: description, 189 | ExecName: packageName, 190 | }) 191 | 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | 196 | hash, err := getFileHash(output.Name()) 197 | if err != nil { 198 | log.Fatal(err) 199 | } 200 | 201 | files = append(files, pkgFile{ 202 | Name: serviceName, 203 | Path: filepath.Join( 204 | "usr/lib/systemd/system/", 205 | serviceName, 206 | ), 207 | Hash: hash, 208 | }) 209 | } 210 | 211 | output, err := os.Create(filepath.Join(dirName, outputName)) 212 | if err != nil { 213 | log.Fatal(err) 214 | } 215 | 216 | err = createPkgbuild(output, pkgData{ 217 | Maintainer: maintainer, 218 | PkgName: packageName, 219 | PkgRel: packageRelease, 220 | ProgramName: strings.TrimSuffix(packageName, "-git"), 221 | RepoURL: safeRepoURL, 222 | License: license, 223 | PkgDesc: description, 224 | Files: files, 225 | Backup: backup, 226 | IsWildcardBuild: isWildcardBuild, 227 | VersionVarName: versionVarName, 228 | Dependencies: dependencies, 229 | MakeDependencies: makeDependencies, 230 | }) 231 | if err != nil { 232 | log.Fatal(err) 233 | } 234 | 235 | if doCreateGitignore { 236 | err = createGitignore(dirName, packageName) 237 | if err != nil { 238 | log.Fatal(err) 239 | } 240 | } 241 | 242 | if doRunBuild { 243 | err = runBuild(dirName, doCleanUp) 244 | if err != nil { 245 | log.Fatal(err) 246 | } 247 | } 248 | 249 | if doCleanUp { 250 | err = cleanUp(dirName, packageName) 251 | if err != nil { 252 | log.Fatal(err) 253 | } 254 | } 255 | } 256 | 257 | func runBuild(dir string, cleanUp bool) error { 258 | logStep("Running makepkg...") 259 | 260 | args := []string{"-f"} 261 | if cleanUp { 262 | args = append(args, "-c") 263 | } 264 | 265 | cmd := exec.Command("makepkg", args...) 266 | cmd.Stdin = os.Stdin 267 | cmd.Stdout = os.Stdout 268 | cmd.Stderr = os.Stderr 269 | cmd.Dir = dir 270 | 271 | err := cmd.Run() 272 | if err != nil { 273 | return err 274 | } 275 | 276 | return nil 277 | } 278 | 279 | func cleanUp(dir, pkgName string) error { 280 | return os.RemoveAll(filepath.Join(dir, pkgName)) 281 | } 282 | 283 | func copyLocalFiles(files []pkgFile, outDir string) error { 284 | logStep("Preparing local files...") 285 | for _, file := range files { 286 | logSubStep("Including file in the package: %s", file.Path) 287 | 288 | targetName := filepath.Join(outDir, file.Name) 289 | 290 | _, err := os.Stat(targetName) 291 | if err != nil { 292 | if !os.IsNotExist(err) { 293 | return err 294 | } 295 | } else { 296 | continue 297 | } 298 | 299 | err = os.Link(file.Path, targetName) 300 | if err != nil { 301 | return err 302 | } 303 | } 304 | 305 | return nil 306 | } 307 | 308 | func createOutputDir(dirName string) error { 309 | if _, err := os.Stat(dirName); !os.IsNotExist(err) { 310 | return err 311 | } 312 | 313 | err := os.Mkdir(dirName, 0755) 314 | if err != nil { 315 | return err 316 | } 317 | 318 | return nil 319 | } 320 | 321 | func createPkgbuild(output io.Writer, data pkgData) error { 322 | logStep("Creating PKGBUILD...") 323 | return pkgbuildTemplate.Execute(output, data) 324 | } 325 | 326 | func createServiceFile(output io.Writer, data serviceData) error { 327 | logStep("Creating service file...") 328 | return serviceTemplate.Execute(output, data) 329 | } 330 | 331 | func createGitignore(dirName string, pkgName string) error { 332 | logStep("Creating .gitignore...") 333 | 334 | ignoreFiles := []string{ 335 | "/*.tar.xz", 336 | "/pkg", 337 | "/src", 338 | "/" + pkgName, 339 | } 340 | 341 | contents := strings.Join(ignoreFiles, "\n") + "\n" 342 | 343 | return ioutil.WriteFile( 344 | filepath.Join(dirName, ".gitignore"), []byte(contents), 0644, 345 | ) 346 | } 347 | 348 | func prepareFileList(names []string, outDir string) ([]pkgFile, error) { 349 | files := []pkgFile{} 350 | 351 | for _, name := range names { 352 | stat, err := os.Stat(name) 353 | if os.IsExist(err) { 354 | continue 355 | } 356 | 357 | if err != nil { 358 | return nil, err 359 | } 360 | 361 | if stat.IsDir() { 362 | continue 363 | } 364 | 365 | if name == "PKGBUILD" { 366 | continue 367 | } 368 | 369 | if strings.HasPrefix(name, outDir) { 370 | continue 371 | } 372 | 373 | hash, err := getFileHash(name) 374 | if err != nil { 375 | return nil, err 376 | } 377 | 378 | files = append(files, pkgFile{ 379 | Path: name, 380 | Name: path.Base(name), 381 | Hash: hash, 382 | }) 383 | } 384 | 385 | return files, nil 386 | } 387 | 388 | func getFileHash(path string) (string, error) { 389 | hash := md5.New() 390 | file, err := os.Open(path) 391 | if err != nil { 392 | return "", err 393 | } 394 | 395 | _, err = io.Copy(hash, file) 396 | if err != nil { 397 | return "", err 398 | } 399 | 400 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 401 | } 402 | 403 | func createBackupList(files []pkgFile) []string { 404 | logStep("Checking backup files...") 405 | 406 | backup := []string{} 407 | for _, file := range files { 408 | logSubStep("Adding to backup: %s", file.Path) 409 | if strings.HasPrefix(file.Path, "etc/") { 410 | backup = append(backup, file.Path) 411 | } 412 | } 413 | 414 | return backup 415 | } 416 | 417 | func getPackageNameFromRepoURL(repo string) string { 418 | base := path.Base(repo) 419 | ext := path.Ext(base) 420 | return strings.TrimSuffix(base, ext) 421 | } 422 | 423 | func trimWildcardFromRepoURL(repo string) (string, bool) { 424 | safeURL := strings.TrimSuffix(repo, "/...") 425 | return safeURL, safeURL != repo 426 | } 427 | 428 | func logSubStep(msg string, data ...interface{}) { 429 | fmt.Printf(" \x1b[1;34m-> \x1b[39m%s\n", fmt.Sprintf(msg, data...)) 430 | } 431 | 432 | func logStep(msg string, data ...interface{}) { 433 | fmt.Printf("\x1b[1;32m==> \x1b[39m%s\n", fmt.Sprintf(msg, data...)) 434 | } 435 | 436 | func replaceUsageDefaults(usage string) string { 437 | maintainer, _ := getMaintainerInfo() 438 | if maintainer != "" { 439 | maintainer = " [default: " + maintainer + "]" 440 | } 441 | 442 | return strings.Replace(usage, "$MAINTAINER", maintainer, -1) 443 | } 444 | 445 | func getMaintainerInfo() (string, error) { 446 | cmdName := exec.Command("git", "config", "--global", "user.name") 447 | name, err := cmdName.CombinedOutput() 448 | if err != nil { 449 | return "", err 450 | } 451 | 452 | cmdEmail := exec.Command("git", "config", "--global", "user.email") 453 | email, err := cmdEmail.CombinedOutput() 454 | if err != nil { 455 | return "", err 456 | } 457 | 458 | return strings.TrimSpace(string(name)) + 459 | " <" + strings.TrimSpace(string(email)) + ">", nil 460 | } 461 | -------------------------------------------------------------------------------- /pkgbuild.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "text/template" 4 | 5 | var pkgbuildTemplate = template.Must( 6 | template.New("pkgbuild").Parse( 7 | `{{if ne .Maintainer ""}}# Maintainer: {{.Maintainer}} 8 | {{end}}pkgname={{.PkgName}} 9 | _pkgname={{.ProgramName}} 10 | pkgver=${PKGVER:-autogenerated} 11 | pkgrel={{if eq .PkgRel "1"}}${PKGREL:-1}{{else}}{{.PkgRel}}{{end}} 12 | pkgdesc="{{.PkgDesc}}" 13 | arch=('i686' 'x86_64') 14 | license=('{{.License}}') 15 | depends=({{range .Dependencies}} 16 | '{{.}}'{{end}} 17 | ) 18 | makedepends=( 19 | 'go' 20 | 'git'{{range .MakeDependencies}} 21 | '{{.}}'{{end}} 22 | ) 23 | 24 | source=( 25 | "$_pkgname::git+{{.RepoURL}}#branch=${BRANCH:-master}"{{range .Files}} 26 | "{{.Name}}"{{end}} 27 | ) 28 | 29 | md5sums=( 30 | 'SKIP'{{range .Files}} 31 | '{{.Hash}}'{{end}} 32 | ) 33 | 34 | backup=({{range .Backup}} 35 | "{{.}}"{{end}} 36 | ) 37 | 38 | pkgver() { 39 | if [[ "$PKGVER" ]]; then 40 | echo "$PKGVER" 41 | return 42 | fi 43 | 44 | cd "$srcdir/$_pkgname" 45 | local date=$(git log -1 --format="%cd" --date=short | sed s/-//g) 46 | local count=$(git rev-list --count HEAD) 47 | local commit=$(git rev-parse --short HEAD) 48 | echo "$date.${count}_$commit" 49 | } 50 | 51 | build() { 52 | cd "$srcdir/$_pkgname" 53 | 54 | if [ -L "$srcdir/$_pkgname" ]; then 55 | rm "$srcdir/$_pkgname" -rf 56 | mv "$srcdir/go/src/$_pkgname/" "$srcdir/$_pkgname" 57 | fi 58 | 59 | rm -rf "$srcdir/go/src" 60 | 61 | mkdir -p "$srcdir/go/src" 62 | 63 | export GOPATH="$srcdir/go" 64 | 65 | mv "$srcdir/$_pkgname" "$srcdir/go/src/" 66 | 67 | cd "$srcdir/go/src/$_pkgname/" 68 | ln -sf "$srcdir/go/src/$_pkgname/" "$srcdir/$_pkgname" 69 | 70 | echo ":: Updating git submodules" 71 | git submodule update --init 72 | 73 | echo ":: Building binary" 74 | go get -v \ 75 | -gcflags "-trimpath $GOPATH/src"{{if ne .VersionVarName ""}} \ 76 | -ldflags="-X main.{{.VersionVarName}}=$pkgver-$pkgrel"{{end}}{{if .IsWildcardBuild}} \ 77 | ./...{{end}} 78 | } 79 | 80 | package() { 81 | find "$srcdir/go/bin/" -type f -executable | while read filename; do 82 | install -DT "$filename" "$pkgdir/usr/bin/$(basename $filename)" 83 | done{{range .Files}} 84 | install -DT -m0755 "$srcdir/{{.Name}}" "$pkgdir/{{.Path}}"{{end}} 85 | } 86 | `)) 87 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "text/template" 4 | 5 | var serviceTemplate = template.Must( 6 | template.New("service").Parse(`[Unit] 7 | Description={{.Description}} 8 | 9 | [Service] 10 | ExecStart=/usr/bin/{{.ExecName}} 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | `)) 16 | --------------------------------------------------------------------------------