//===-- llvm-go.go - go tool wrapper for LLVM -----------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This tool lets us build LLVM components within the tree by setting up a
// $GOPATH that resembles a tree fetched in the normal way with "go get".
//
//===----------------------------------------------------------------------===//

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

const (
	linkmodeComponentLibs = "component-libs"
	linkmodeDylib         = "dylib"
)

type pkg struct {
	llvmpath, pkgpath string
}

var packages = []pkg{
	{"bindings/go/llvm", "llvm.org/llvm/bindings/go/llvm"},
}

type compilerFlags struct {
	cpp, cxx, ld string
}

var components = []string{
	"all-targets",
	"analysis",
	"asmparser",
	"asmprinter",
	"bitreader",
	"bitwriter",
	"codegen",
	"core",
	"coroutines",
	"debuginfodwarf",
	"executionengine",
	"instrumentation",
	"interpreter",
	"ipo",
	"irreader",
	"linker",
	"mc",
	"mcjit",
	"objcarcopts",
	"option",
	"profiledata",
	"scalaropts",
	"support",
	"target",
}

func llvmConfig(args ...string) string {
	configpath := os.Getenv("LLVM_CONFIG")
	if configpath == "" {
		bin, _ := filepath.Split(os.Args[0])
		configpath = filepath.Join(bin, "llvm-config")
	}

	cmd := exec.Command(configpath, args...)
	cmd.Stderr = os.Stderr
	out, err := cmd.Output()
	if err != nil {
		panic(err.Error())
	}

	outstr := string(out)
	outstr = strings.TrimSuffix(outstr, "\n")
	outstr = strings.Replace(outstr, "\n", " ", -1)
	return outstr
}

func llvmFlags() compilerFlags {
	args := append([]string{"--ldflags", "--libs", "--system-libs"}, components...)
	ldflags := llvmConfig(args...)
	stdLibOption := ""
	if strings.Contains(llvmConfig("--cxxflags"), "-stdlib=libc++") {
		// If libc++ is used to build LLVM libraries, -stdlib=libc++ is
		// needed to resolve dependent symbols
		stdLibOption = "-stdlib=libc++"
	}
	if runtime.GOOS != "darwin" {
		// OS X doesn't like -rpath with cgo. See:
		// https://github.com/golang/go/issues/7293
		ldflags = "-Wl,-rpath," + llvmConfig("--libdir") + " " + ldflags
	}
	return compilerFlags{
		cpp: llvmConfig("--cppflags"),
		cxx: "-std=c++14" + " " + stdLibOption,
		ld:  ldflags,
	}
}

func addTag(args []string, tag string) []string {
	args = append([]string{}, args...)
	addedTag := false
	for i, a := range args {
		if strings.HasPrefix(a, "-tags=") {
			args[i] = a + " " + tag
			addedTag = true
		} else if a == "-tags" && i+1 < len(args) {
			args[i+1] = args[i+1] + " " + tag
			addedTag = true
		}
	}
	if !addedTag {
		args = append([]string{args[0], "-tags", tag}, args[1:]...)
	}
	return args
}

func printComponents() {
	fmt.Println(strings.Join(components, " "))
}

func printConfig() {
	flags := llvmFlags()

	fmt.Printf(`// +build !byollvm

// This file is generated by llvm-go, do not edit.

package llvm

/*
#cgo CPPFLAGS: %s
#cgo CXXFLAGS: %s
#cgo LDFLAGS: %s
*/
import "C"

type (run_build_sh int)
`, flags.cpp, flags.cxx, flags.ld)
}

func runGoWithLLVMEnv(args []string, cc, cxx, gocmd, llgo, cppflags, cxxflags, ldflags string, packages []pkg) {
	args = addTag(args, "byollvm")

	srcdir := llvmConfig("--src-root")

	tmpgopath, err := ioutil.TempDir("", "gopath")
	if err != nil {
		panic(err.Error())
	}

	for _, p := range packages {
		path := filepath.Join(tmpgopath, "src", p.pkgpath)
		err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
		if err != nil {
			panic(err.Error())
		}

		abspath := p.llvmpath
		if !filepath.IsAbs(abspath) {
			abspath = filepath.Join(srcdir, abspath)
		}

		err = os.Symlink(abspath, path)
		if err != nil {
			panic(err.Error())
		}
	}

	newpath := os.Getenv("PATH")

	newgopathlist := []string{tmpgopath}
	newgopathlist = append(newgopathlist, filepath.SplitList(os.Getenv("GOPATH"))...)
	newgopath := strings.Join(newgopathlist, string(filepath.ListSeparator))

	flags := llvmFlags()

	newenv := []string{
		"CC=" + cc,
		"CXX=" + cxx,
		"CGO_CPPFLAGS=" + flags.cpp + " " + cppflags,
		"CGO_CXXFLAGS=" + flags.cxx + " " + cxxflags,
		"CGO_LDFLAGS=" + flags.ld + " " + ldflags,
		"GOPATH=" + newgopath,
		"PATH=" + newpath,
	}
	if llgo != "" {
		newenv = append(newenv, "GCCGO="+llgo)
	}

	for _, v := range os.Environ() {
		if !strings.HasPrefix(v, "CC=") &&
			!strings.HasPrefix(v, "CXX=") &&
			!strings.HasPrefix(v, "CGO_CPPFLAGS=") &&
			!strings.HasPrefix(v, "CGO_CXXFLAGS=") &&
			!strings.HasPrefix(v, "CGO_LDFLAGS=") &&
			!strings.HasPrefix(v, "GCCGO=") &&
			!strings.HasPrefix(v, "GOPATH=") &&
			!strings.HasPrefix(v, "PATH=") {
			newenv = append(newenv, v)
		}
	}

	gocmdpath, err := exec.LookPath(gocmd)
	if err != nil {
		panic(err.Error())
	}

	proc, err := os.StartProcess(gocmdpath, append([]string{gocmd}, args...),
		&os.ProcAttr{
			Env:   newenv,
			Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
		})
	if err != nil {
		panic(err.Error())
	}
	ps, err := proc.Wait()
	if err != nil {
		panic(err.Error())
	}

	os.RemoveAll(tmpgopath)

	if !ps.Success() {
		os.Exit(1)
	}
}

func usage() {
	fmt.Println(`Usage: llvm-go subcommand [flags]

Available subcommands: build get install run test print-components print-config`)
	os.Exit(0)
}

func main() {
	cc := os.Getenv("CC")
	cxx := os.Getenv("CXX")
	cppflags := os.Getenv("CGO_CPPFLAGS")
	cxxflags := os.Getenv("CGO_CXXFLAGS")
	ldflags := os.Getenv("CGO_LDFLAGS")
	gocmd := "go"
	llgo := ""
	packagesString := ""

	flags := []struct {
		name string
		dest *string
	}{
		{"cc", &cc},
		{"cxx", &cxx},
		{"go", &gocmd},
		{"llgo", &llgo},
		{"cppflags", &cppflags},
		{"ldflags", &ldflags},
		{"packages", &packagesString},
	}

	args := os.Args[1:]
LOOP:
	for {
		if len(args) == 0 {
			usage()
		}
		for _, flag := range flags {
			if strings.HasPrefix(args[0], flag.name+"=") {
				*flag.dest = args[0][len(flag.name)+1:]
				args = args[1:]
				continue LOOP
			}
		}
		break
	}

	packages := packages
	if packagesString != "" {
		for _, field := range strings.Fields(packagesString) {
			pos := strings.IndexRune(field, '=')
			if pos == -1 {
				fmt.Fprintf(os.Stderr, "invalid packages value %q, expected 'pkgpath=llvmpath [pkgpath=llvmpath ...]'\n", packagesString)
				os.Exit(1)
			}
			packages = append(packages, pkg{
				pkgpath:  field[:pos],
				llvmpath: field[pos+1:],
			})
		}
	}

	switch args[0] {
	case "build", "get", "install", "run", "test":
		runGoWithLLVMEnv(args, cc, cxx, gocmd, llgo, cppflags, cxxflags, ldflags, packages)
	case "print-components":
		printComponents()
	case "print-config":
		printConfig()
	default:
		usage()
	}
}
