1 /**
2 	Build platform identification and speficiation matching.
3 
4 	This module is useful for determining the build platform for a certain
5 	machine and compiler invocation. Example applications include classifying
6 	CI slave machines.
7 
8 	It also contains means to match build platforms against a platform
9 	specification string as used in package reciptes.
10 
11 	Copyright: © 2012-2016 rejectedsoftware e.K.
12 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
13 	Authors: Sönke Ludwig
14 */
15 module dub.platform;
16 
17 import std.array;
18 
19 
20 /** Determines the full build platform used for the current build.
21 
22 	Note that the `BuildPlatform.compilerBinary` field will be left empty.
23 
24 	See_Also: `determinePlatform`, `determineArchitecture`, `determineCompiler`
25 */
26 BuildPlatform determineBuildPlatform()
27 {
28 	BuildPlatform ret;
29 	ret.platform = determinePlatform();
30 	ret.architecture = determineArchitecture();
31 	ret.compiler = determineCompiler();
32 	ret.frontendVersion = __VERSION__;
33 	return ret;
34 }
35 
36 
37 /** Returns a list of platform identifiers that apply to the current
38 	build.
39 
40 	Example results are `["windows"]` or `["posix", "osx"]`. The identifiers
41 	correspond to the compiler defined version constants built into the
42 	language, except that they are converted to lower case.
43 
44 	See_Also: `determineBuildPlatform`
45 */
46 string[] determinePlatform()
47 {
48 	auto ret = appender!(string[])();
49 	version(Windows) ret.put("windows");
50 	version(linux) ret.put("linux");
51 	version(Posix) ret.put("posix");
52 	version(OSX) ret.put("osx");
53 	version(FreeBSD) ret.put("freebsd");
54 	version(OpenBSD) ret.put("openbsd");
55 	version(NetBSD) ret.put("netbsd");
56 	version(DragonFlyBSD) ret.put("dragonflybsd");
57 	version(BSD) ret.put("bsd");
58 	version(Solaris) ret.put("solaris");
59 	version(AIX) ret.put("aix");
60 	version(Haiku) ret.put("haiku");
61 	version(SkyOS) ret.put("skyos");
62 	version(SysV3) ret.put("sysv3");
63 	version(SysV4) ret.put("sysv4");
64 	version(Hurd) ret.put("hurd");
65 	version(Android) ret.put("android");
66 	version(Cygwin) ret.put("cygwin");
67 	version(MinGW) ret.put("mingw");
68 	return ret.data;
69 }
70 
71 /** Returns a list of architecture identifiers that apply to the current
72 	build.
73 
74 	Example results are `["x86_64"]` or `["arm", "arm_softfloat"]`. The
75 	identifiers correspond to the compiler defined version constants built into
76 	the language, except that they are converted to lower case.
77 
78 	See_Also: `determineBuildPlatform`
79 */
80 string[] determineArchitecture()
81 {
82 	auto ret = appender!(string[])();
83 	version(X86) ret.put("x86");
84 	version(X86_64) ret.put("x86_64");
85 	version(ARM) ret.put("arm");
86 	version(ARM_Thumb) ret.put("arm_thumb");
87 	version(ARM_SoftFloat) ret.put("arm_softfloat");
88 	version(ARM_HardFloat) ret.put("arm_hardfloat");
89 	version(ARM64) ret.put("arm64");
90 	version(PPC) ret.put("ppc");
91 	version(PPC_SoftFP) ret.put("ppc_softfp");
92 	version(PPC_HardFP) ret.put("ppc_hardfp");
93 	version(PPC64) ret.put("ppc64");
94 	version(IA64) ret.put("ia64");
95 	version(MIPS) ret.put("mips");
96 	version(MIPS32) ret.put("mips32");
97 	version(MIPS64) ret.put("mips64");
98 	version(MIPS_O32) ret.put("mips_o32");
99 	version(MIPS_N32) ret.put("mips_n32");
100 	version(MIPS_O64) ret.put("mips_o64");
101 	version(MIPS_N64) ret.put("mips_n64");
102 	version(MIPS_EABI) ret.put("mips_eabi");
103 	version(MIPS_NoFloat) ret.put("mips_nofloat");
104 	version(MIPS_SoftFloat) ret.put("mips_softfloat");
105 	version(MIPS_HardFloat) ret.put("mips_hardfloat");
106 	version(SPARC) ret.put("sparc");
107 	version(SPARC_V8Plus) ret.put("sparc_v8plus");
108 	version(SPARC_SoftFP) ret.put("sparc_softfp");
109 	version(SPARC_HardFP) ret.put("sparc_hardfp");
110 	version(SPARC64) ret.put("sparc64");
111 	version(S390) ret.put("s390");
112 	version(S390X) ret.put("s390x");
113 	version(HPPA) ret.put("hppa");
114 	version(HPPA64) ret.put("hppa64");
115 	version(SH) ret.put("sh");
116 	version(SH64) ret.put("sh64");
117 	version(Alpha) ret.put("alpha");
118 	version(Alpha_SoftFP) ret.put("alpha_softfp");
119 	version(Alpha_HardFP) ret.put("alpha_hardfp");
120 	return ret.data;
121 }
122 
123 /** Determines the canonical compiler name used for the current build.
124 
125 	The possible values currently are "dmd", "gdc", "ldc2" or "sdc". If an
126 	unknown compiler is used, this function will return an empty string.
127 
128 	See_Also: `determineBuildPlatform`
129 */
130 string determineCompiler()
131 {
132 	version(DigitalMars) return "dmd";
133 	else version(GNU) return "gdc";
134 	else version(LDC) return "ldc2";
135 	else version(SDC) return "sdc";
136 	else return null;
137 }
138 
139 /** Matches a platform specification string against a build platform.
140 
141 	Specifications are build upon the following scheme, where each component
142 	is optional (indicated by []), but the order is obligatory:
143 	"[-platform][-architecture][-compiler]"
144 
145 	So the following strings are valid specifications: `"-windows-x86-dmd"`,
146 	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
147 	
148 	Params:
149 		platform = The build platform to match agains the platform specification
150 	    specification = The specification being matched. It must either be an
151 	    	empty string or start with a dash.
152 
153 	Returns:
154 	    `true` if the given specification matches the build platform, `false`
155 	    otherwise. Using an empty string as the platform specification will
156 	    always result in a match.
157 */
158 bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
159 {
160 	import std.string : format;
161 	import std.algorithm : canFind, splitter;
162 	import std.exception : enforce;
163 
164 	if (specification.empty) return true;
165 	if (platform == BuildPlatform.any) return true;
166 
167 	auto splitted = specification.splitter('-');
168 	assert(!splitted.empty, "No valid platform specification! The leading hyphen is required!");
169 	splitted.popFront(); // Drop leading empty match.
170 	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
171 
172 	if (platform.platform.canFind(splitted.front)) {
173 		splitted.popFront();
174 		if (splitted.empty)
175 			return true;
176 	}
177 	if (platform.architecture.canFind(splitted.front)) {
178 		splitted.popFront();
179 		if (splitted.empty)
180 			return true;
181 	}
182 	if (platform.compiler == splitted.front) {
183 		splitted.popFront();
184 		enforce(splitted.empty, "No valid specification! The compiler has to be the last element!");
185 		return true;
186 	}
187 	return false;
188 }
189 
190 ///
191 unittest {
192 	auto platform=BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
193 	assert(platform.matchesSpecification(""));
194 	assert(platform.matchesSpecification("-posix"));
195 	assert(platform.matchesSpecification("-linux"));
196 	assert(platform.matchesSpecification("-linux-dmd"));
197 	assert(platform.matchesSpecification("-linux-x86_64-dmd"));
198 	assert(platform.matchesSpecification("-x86_64"));
199 	assert(!platform.matchesSpecification("-windows"));
200 	assert(!platform.matchesSpecification("-ldc"));
201 	assert(!platform.matchesSpecification("-windows-dmd"));
202 }
203 
204 /// Represents a platform a package can be build upon.
205 struct BuildPlatform {
206 	/// Special constant used to denote matching any build platform.
207 	enum any = BuildPlatform(null, null, null, null, -1);
208 
209 	/// Platform identifiers, e.g. ["posix", "windows"]
210 	string[] platform;
211 	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
212 	string[] architecture;
213 	/// Canonical compiler name e.g. "dmd"
214 	string compiler;
215 	/// Compiler binary name e.g. "ldmd2"
216 	string compilerBinary;
217 	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
218 	int frontendVersion;
219 }