1// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Package llvm provides functions and types for locating and using the llvm
16// toolchains.
17package llvm
18
19import (
20	"bytes"
21	"fmt"
22	"io/ioutil"
23	"net/http"
24	"os"
25	"os/exec"
26	"path/filepath"
27	"regexp"
28	"runtime"
29	"sort"
30	"strconv"
31
32	"../util"
33)
34
35const maxLLVMVersion = 10
36
37// Version holds the build version information of an LLVM toolchain.
38type Version struct {
39	Major, Minor, Point int
40}
41
42func (v Version) String() string {
43	return fmt.Sprintf("%v.%v.%v", v.Major, v.Minor, v.Point)
44}
45
46// GreaterEqual returns true if v >= rhs.
47func (v Version) GreaterEqual(rhs Version) bool {
48	if v.Major > rhs.Major {
49		return true
50	}
51	if v.Major < rhs.Major {
52		return false
53	}
54	if v.Minor > rhs.Minor {
55		return true
56	}
57	if v.Minor < rhs.Minor {
58		return false
59	}
60	return v.Point >= rhs.Point
61}
62
63// Download downloads and verifies the LLVM toolchain for the current OS.
64func (v Version) Download() ([]byte, error) {
65	return v.DownloadForOS(runtime.GOOS)
66}
67
68// DownloadForOS downloads and verifies the LLVM toolchain for the given OS.
69func (v Version) DownloadForOS(osName string) ([]byte, error) {
70	url, sig, key, err := v.DownloadInfoForOS(osName)
71	if err != nil {
72		return nil, err
73	}
74
75	resp, err := http.Get(url)
76	if err != nil {
77		return nil, fmt.Errorf("Could not download LLVM from %v: %v", url, err)
78	}
79	defer resp.Body.Close()
80
81	content, err := ioutil.ReadAll(resp.Body)
82	if err != nil {
83		return nil, fmt.Errorf("Could not download LLVM from %v: %v", url, err)
84	}
85
86	sigfile, err := os.Open(sig)
87	if err != nil {
88		return nil, fmt.Errorf("Couldn't open file '%s': %v", sig, err)
89	}
90	defer sigfile.Close()
91
92	keyfile, err := os.Open(key)
93	if err != nil {
94		return nil, fmt.Errorf("Couldn't open file '%s': %v", key, err)
95	}
96	defer keyfile.Close()
97
98	if err := util.CheckPGP(bytes.NewReader(content), sigfile, keyfile); err != nil {
99		return nil, err
100	}
101	return content, nil
102}
103
104// DownloadInfoForOS returns the download url, signature and key for the given
105// LLVM version for the given OS.
106func (v Version) DownloadInfoForOS(os string) (url, sig, key string, err error) {
107	switch v {
108	case Version{10, 0, 0}:
109		key = relfile("10.0.0.pub.key")
110		switch os {
111		case "linux":
112			url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz"
113			sig = relfile("10.0.0-ubuntu.sig")
114			return
115		case "darwin":
116			url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang+llvm-10.0.0-x86_64-apple-darwin.tar.xz"
117			sig = relfile("10.0.0-darwin.sig")
118			return
119		case "windows":
120			url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/LLVM-10.0.0-win64.exe"
121			sig = relfile("10.0.0-win64.sig")
122			return
123		default:
124			return "", "", "", fmt.Errorf("Unsupported OS: %v", os)
125		}
126	default:
127		return "", "", "", fmt.Errorf("Unknown download for LLVM %v", v)
128	}
129}
130func relfile(path string) string {
131	_, thisFile, _, _ := runtime.Caller(1)
132	thisDir := filepath.Dir(thisFile)
133	return filepath.Join(thisDir, path)
134}
135
136// Toolchain holds the paths and version information about an LLVM toolchain.
137type Toolchain struct {
138	Version Version
139	BinDir  string
140}
141
142// Toolchains is a list of Toolchain
143type Toolchains []Toolchain
144
145// Find looks for a toolchain with the specific version.
146func (l Toolchains) Find(v Version) *Toolchain {
147	for _, t := range l {
148		if t.Version == v {
149			return &t
150		}
151	}
152	return nil
153}
154
155// FindAtLeast looks for a toolchain with the given version, returning the highest found version.
156func (l Toolchains) FindAtLeast(v Version) *Toolchain {
157	out := (*Toolchain)(nil)
158	for _, t := range l {
159		if t.Version.GreaterEqual(v) && (out == nil || out.Version.GreaterEqual(t.Version)) {
160			t := t
161			out = &t
162		}
163	}
164	return out
165}
166
167// Search looks for llvm toolchains in paths.
168// If paths is empty, then PATH is searched.
169func Search(paths ...string) Toolchains {
170	toolchains := map[Version]Toolchain{}
171	search := func(name string) {
172		if len(paths) > 0 {
173			for _, path := range paths {
174				if util.IsFile(path) {
175					path = filepath.Dir(path)
176				}
177				if t := toolchain(path); t != nil {
178					toolchains[t.Version] = *t
179					continue
180				}
181				if t := toolchain(filepath.Join(path, "bin")); t != nil {
182					toolchains[t.Version] = *t
183					continue
184				}
185			}
186		} else {
187			path, err := exec.LookPath(name)
188			if err == nil {
189				if t := toolchain(filepath.Dir(path)); t != nil {
190					toolchains[t.Version] = *t
191				}
192			}
193		}
194	}
195
196	search("clang")
197	for i := 8; i < maxLLVMVersion; i++ {
198		search(fmt.Sprintf("clang-%d", i))
199	}
200
201	out := make([]Toolchain, 0, len(toolchains))
202	for _, t := range toolchains {
203		out = append(out, t)
204	}
205	sort.Slice(out, func(i, j int) bool { return out[i].Version.GreaterEqual(out[j].Version) })
206
207	return out
208}
209
210// Clang returns the path to the clang executable.
211func (t Toolchain) Clang() string {
212	return filepath.Join(t.BinDir, "clang"+exeExt())
213}
214
215// ClangXX returns the path to the clang++ executable.
216func (t Toolchain) ClangXX() string {
217	return filepath.Join(t.BinDir, "clang++"+exeExt())
218}
219
220// Cov returns the path to the llvm-cov executable.
221func (t Toolchain) Cov() string {
222	return filepath.Join(t.BinDir, "llvm-cov"+exeExt())
223}
224
225// Profdata returns the path to the llvm-profdata executable.
226func (t Toolchain) Profdata() string {
227	return filepath.Join(t.BinDir, "llvm-profdata"+exeExt())
228}
229
230func toolchain(dir string) *Toolchain {
231	t := Toolchain{BinDir: dir}
232	if t.resolve() {
233		return &t
234	}
235	return nil
236}
237
238func (t *Toolchain) resolve() bool {
239	if !util.IsFile(t.Profdata()) { // llvm-profdata doesn't have --version flag
240		return false
241	}
242	version, ok := parseVersion(t.Cov())
243	t.Version = version
244	return ok
245}
246
247func exeExt() string {
248	switch runtime.GOOS {
249	case "windows":
250		return ".exe"
251	default:
252		return ""
253	}
254}
255
256var versionRE = regexp.MustCompile(`(?:clang|LLVM) version ([0-9]+)\.([0-9]+)\.([0-9]+)`)
257
258func parseVersion(tool string) (Version, bool) {
259	out, err := exec.Command(tool, "--version").Output()
260	if err != nil {
261		return Version{}, false
262	}
263	matches := versionRE.FindStringSubmatch(string(out))
264	if len(matches) < 4 {
265		return Version{}, false
266	}
267	major, majorErr := strconv.Atoi(matches[1])
268	minor, minorErr := strconv.Atoi(matches[2])
269	point, pointErr := strconv.Atoi(matches[3])
270	if majorErr != nil || minorErr != nil || pointErr != nil {
271		return Version{}, false
272	}
273	return Version{major, minor, point}, true
274}
275