Skip to content

Commit

Permalink
First working version
Browse files Browse the repository at this point in the history
  • Loading branch information
rjeczalik committed May 31, 2014
1 parent 429b216 commit d4a343f
Show file tree
Hide file tree
Showing 18 changed files with 1,388 additions and 0 deletions.
175 changes: 175 additions & 0 deletions cmd/pkg-config/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// cmd/pkg-config is a Go-centric and GOPATH-aware pkg-config replacement
// for use with the cgo tool.
//
// Using cmd/pkg-config with the cgo command
//
// To use cmd/pkg-config go install it and ensure it's in the PATH. A NOTE for
// Linux users: the cmd/pkg-config must be present before original
// /usr/bin/pkg-config in the PATH list; it is not advised to replace it,
// as cmd/pkg-config is not a full replacement for the pkg-config tool, e.g.
// it does not implement conflict and dependency resolving.
//
// The cgo tool uses pkg-config for obtaining CFLAGS and LDFLAGS of C libraries.
// Example:
//
// // #cgo pkg-config: libpng
// // #include <png.h>
// import "C"
//
// The "#cgo pkg-config: png" directive makes a cgo tool query a pkg-config for
// CFLAGS during generation of a C wrapper (--cflags flag) and for LDFLAGS during
// linking (--libs flag). The original pkg-config looks up a libpng.pc file in
// a directory list specified by the PKG_CONFIG_PATH environment variable plus
// a few other default ones. The libpng.pc file can have the following content:
//
// prefix=/usr
// exec_prefix=${prefix}
// libdir=${prefix}/lib/x86_64-linux-gnu
// includedir=${prefix}/include/libpng12
//
// Name: libpng
// Description: Loads and saves PNG files
// Version: 1.2.46
// Libs: -L${libdir} -lpng12
// Libs.private: -lz -lm
// Cflags: -I${includedir}
//
// A .pc file is composed of two parts - variables and keywords. Keywords may
// reference pre-declared variables.
//
// $ pkg-config --cflags libpng
// -I/usr/include/libpng12
// $ pkg-config --libs libpng
// -L/usr/lib/x86_64-linux-gnu -lpng12
// $ pkg-config --cflags --libs libpng
// -I/usr/include/libpng12 -L/usr/lib/x86_64-linux-gnu -lpng12
//
// The cmd/pkg-config tool looks up for a PC file in two other places in addition
// do the original pkg-config: $GOPATH and github.com.
//
// The cmd/pkg-config tool and $GOPATH
//
// The cmd/pkg-config defines standard directory layout for C libraries:
//
// - $GOPATH/lib contains both the .pc files and binaries
// - $GOPATH/include contains package headers
//
// The $GOPATH tree for the example may look like the following:
//
// $GOPATH
// ├── include
// │   └── libpng
// │   └── libpng12
// │   ├── pngconf.h
// │   └── png.h
// ├── lib
// │   ├── linux_amd64
// │   │   └── libpng
// │   │   ├── libpng12.so
// │   │   └── libpng.pc
// │   └── windows_amd64
// │   └── libpng
// │   ├── libpng12.dll
// │   ├── libpng12.dll.a
// │   └── libpng.pc
// └── src
// └── github.com
// └── joe
// └── png-wrapper
// └── png-wrapper.go
//
// The cmd/pkg-config reads the libpng.pc file from $GOPATH/lib/libpng/$GOOS_$GOARCH/libpng.pc.
// The .pc file written for cmd/pkg-config can use $GOPATH, $GOOS and $GOARCH
// builtin variables, which are expanded by the cmd/pkg-config during runtime.
// The rewritten .pc file for libpng may look like the following:
//
// libdir=${GOPATH}/lib/${GOOS}_${GOARCH}/libpng
// includedir=${GOPATH}/include/libpng
//
// Name: libpng
// Description: Loads and saves PNG files
// Version: 1.2.46
// Libs: -L${libdir} -lpng12
// Libs.private: -lz -lm
// Cflags: -I${includedir}
//
// The cmd/pkg-config tool and github.com
//
// Although it's advised to always use an official or self-compiled libraries for
// a production use, cmd/pkg-config can download a zip archive from project's
// github.com releases for a pkg-config tag and unpack it into $GOPATH.
// For example in order to make the above github.com/joe/png-wrapper package
// pkg-config-gettable, it's enough to zip include/ and lib/ directories:
//
// $ zip -9 -r libpng.zip include dir
//
// Create release, name a tag after pkg-config and attach libpng.zip do the
// file list. This would make the libpng.zip archive be accessible from the following
// link:
//
// $ wget http://github.com/joe/png-wrapper/releases/download/pkg-config/libpng.zip
//
// Which is the default location the cmd/pkg-config searches for libraries. Then
// go-getting a joe/png-wrapper package altogether with C dependencies is as
// easy as:
//
// $ go get github.com/joe/png-wrapper
//
// The cmd/pkg-config tool looks up a .pc file for a $LIBRARY in the following order:
//
// - $GOPATH/lib/$GOOS_$GOARCH/$LIBRARY/$LIBRARY.pc
// - http://github.com/$USER/$PROJECT/releases/download/pkg-config/$LIBRARY.zip
// - $PKG_CONFIG_PATH and eventual pkg-config's default search locations (platform-specific)
package main

import (
"fmt"
"os"

"github.com/rjeczalik/pkgconfig"
)

const USAGE = `NAME:
pkg-config - Go-centric pkg-config replacement
USAGE:
pkg-config --libs PKG
pkg-config --cflags PKG
pkg-config --cflags --libs PKG1 PKG2
pkg-config get github.com/USER/PROJ PKG`

func die(v ...interface{}) {
for _, v := range v {
fmt.Fprintln(os.Stderr, v)
}
os.Exit(1)
}

func ishelp(s string) bool {
return s == "-h" || s == "-help" || s == "help" || s == "--help" || s == "/?"
}

func main() {
if len(os.Args) == 1 || (len(os.Args) == 2 && ishelp(os.Args[1])) {
fmt.Println(USAGE)
} else {
switch os.Args[1] {
case "get":
if len(os.Args) != 4 {
die(USAGE)
}
pc, err := pkgconfig.LookupGithubProj(os.Args[3], os.Args[2])
if err != nil {
die(err)
}
_ = pc
default:
pkg := pkgconfig.NewPkgArgs(os.Args[1:])
if err := pkg.Resolve(); err == nil {
pkg.WriteTo(os.Stdout)
} else {
die(err)
}
}
}
}
188 changes: 188 additions & 0 deletions github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package pkgconfig

import (
"archive/zip"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)

var githubProj string

func init() {
if wd, err := os.Getwd(); err == nil {
githubProj = extractproj(wd, os.PathSeparator)
}
}

var src = map[rune]string{'/': "/src/", '\\': `\src\`}

var projpath = map[rune]func(string) string{
'/': func(s string) string { return s },
'\\': func(s string) string { return strings.Replace(s, "\\", "/", -1) },
}

func extractproj(path string, sep rune) (proj string) {
n := strings.LastIndex(path, src[sep])
if n == -1 {
return
}
n += len(src[sep])
m := n
for i := 0; i < 3; i++ {
l := strings.Index(path[m:], string(sep))
if l == -1 {
m = len(path) + 1
break
}
m += l + 1
}
proj = projpath[sep](string(path[n : m-1]))
if !strings.HasPrefix(proj, "github.com") {
proj = ""
}
return
}

// LookupGithub TODO(rjeczalik): document
func LookupGithub(pkg string) (*PC, error) {
if githubProj == "" {
return nil, errors.New(`unable to guess project's URL from $CWD`)
}
return LookupGithubProj(pkg, githubProj)
}

var targets = map[string]struct{}{
"darwin_386": {},
"darwin_amd64": {},
"freebsd_386": {},
"freebsd_amd64": {},
"freebsd_arm": {},
"linux_386": {},
"linux_amd64": {},
"linux_arm": {},
"netbsd_386": {},
"netbsd_amd64": {},
"netbsd_arm": {},
"openbsd_386": {},
"openbsd_amd64": {},
"plan9_386": {},
"plan9_amd64": {},
"windows_386": {},
"windows_amd64": {},
}

const targetsMinKeyLen = 9

func validFile(path, pkg string) bool {
switch {
case strings.HasPrefix(path, "include/"):
i := len("include/")
if len(path) < i+len(pkg)+1 {
return false
}
n := strings.Index(path[i:], "/")
if n == -1 {
return false
}
return string(path[i:i+n]) == pkg
case strings.HasPrefix(path, "lib/"):
i := len("lib/")
if len(path) < i+targetsMinKeyLen+1+len(pkg)+1 {
return false
}
n := strings.Index(path[i:], "/")
if n == -1 {
return false
}
if _, ok := targets[string(path[i:i+n])]; !ok {
return false
}
i += n + 1
if n = strings.Index(path[i:], "/"); n == -1 {
return false
}
return string(path[i:i+n]) == pkg
}
return false
}

var replacefile = map[rune]func(string) string{
'/': func(s string) string { return s },
'\\': func(s string) string { return strings.Replace(s, "/", "\\", -1) },
}

func copyFile(gopath string, f *zip.File) (err error) {
rc, err := f.Open()
if err != nil {
return
}
defer rc.Close()
dir := filepath.Join(gopath, replacefile[os.PathSeparator](path.Dir(f.Name)))
if err = os.MkdirAll(dir, 0755); err != nil {
return
}
file, err := os.OpenFile(filepath.Join(dir, path.Base(f.Name)), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return
}
_, err = io.Copy(file, rc)
file.Close()
return
}

// LookupGithunProj TODO(rjeczalik): document
func LookupGithubProj(pkg, proj string) (*PC, error) {
path := os.Getenv("GOPATH")
if p := strings.Split(path, string(os.PathListSeparator)); len(path) != 0 {
path = p[0]
}
if path == "" {
return nil, errors.New("$GOPATH is empty")
}
url := fmt.Sprintf("http://%s/releases/download/pkg-config/%s.zip", proj, pkg)
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, errors.New("not found: " + url)
}
f, err := ioutil.TempFile("", pkg)
if err != nil {
return nil, err
}
if _, err = io.Copy(f, res.Body); err != nil {
f.Close()
os.Remove(f.Name())
return nil, err
}
f.Close()
r, err := zip.OpenReader(f.Name())
if err != nil {
return nil, err
}
defer func() {
r.Close()
os.Remove(f.Name())
}()
for _, f := range r.File {
// Filter out directories.
if f.Name[len(f.Name)-1] != '/' {
if !validFile(f.Name, pkg) {
return nil, fmt.Errorf("unexcpected file %q", f.Name)
}
if err = copyFile(path, f); err != nil {
return nil, err
}
}
}
return LookupGopath(pkg)
}
Loading

0 comments on commit d4a343f

Please sign in to comment.