1 /**
2 	Build platform identification and specification 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-2017 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 // archCheck, compilerCheck, and platformCheck are used below and in
20 // generatePlatformProbeFile, so they've been extracted into these strings
21 // that can be reused.
22 // Try to not use phobos in the probes to avoid long import times.
23 /// private
24 enum string platformCheck = q{
25 	string[] ret;
26 	version(Windows) ret ~= "windows";
27 	version(linux) ret ~= "linux";
28 	version(Posix) ret ~= "posix";
29 	version(OSX) ret ~= ["osx", "darwin"];
30 	version(iOS) ret ~= ["ios", "darwin"];
31 	version(TVOS) ret ~= ["tvos", "darwin"];
32 	version(WatchOS) ret ~= ["watchos", "darwin"];
33 	version(FreeBSD) ret ~= "freebsd";
34 	version(OpenBSD) ret ~= "openbsd";
35 	version(NetBSD) ret ~= "netbsd";
36 	version(DragonFlyBSD) ret ~= "dragonflybsd";
37 	version(BSD) ret ~= "bsd";
38 	version(Solaris) ret ~= "solaris";
39 	version(AIX) ret ~= "aix";
40 	version(Haiku) ret ~= "haiku";
41 	version(SkyOS) ret ~= "skyos";
42 	version(SysV3) ret ~= "sysv3";
43 	version(SysV4) ret ~= "sysv4";
44 	version(Hurd) ret ~= "hurd";
45 	version(Android) ret ~= "android";
46 	version(Cygwin) ret ~= "cygwin";
47 	version(MinGW) ret ~= "mingw";
48 	version(PlayStation4) ret ~= "playstation4";
49 	version(WebAssembly) ret ~= "wasm";
50 	return ret;
51 };
52 
53 /// private
54 enum string archCheck = q{
55 	string[] ret;
56 	version(X86) ret ~= "x86";
57 	// Hack: see #1535
58 	// Makes "x86_omf" available as a platform specifier in the package recipe
59 	version(X86) version(CRuntime_DigitalMars) ret ~= "x86_omf";
60 	// Hack: see #1059
61 	// When compiling with --arch=x86_mscoff build_platform.architecture is equal to ["x86"] and canFind below is false.
62 	// This hack prevents unnesessary warning 'Failed to apply the selected architecture x86_mscoff. Got ["x86"]'.
63 	// And also makes "x86_mscoff" available as a platform specifier in the package recipe
64 	version(X86) version(CRuntime_Microsoft) ret ~= "x86_mscoff";
65 	version(X86_64) ret ~= "x86_64";
66 	version(ARM) ret ~= "arm";
67 	version(AArch64) ret ~= "aarch64";
68 	version(ARM_Thumb) ret ~= "arm_thumb";
69 	version(ARM_SoftFloat) ret ~= "arm_softfloat";
70 	version(ARM_HardFloat) ret ~= "arm_hardfloat";
71 	version(PPC) ret ~= "ppc";
72 	version(PPC_SoftFP) ret ~= "ppc_softfp";
73 	version(PPC_HardFP) ret ~= "ppc_hardfp";
74 	version(PPC64) ret ~= "ppc64";
75 	version(IA64) ret ~= "ia64";
76 	version(MIPS) ret ~= "mips";
77 	version(MIPS32) ret ~= "mips32";
78 	version(MIPS64) ret ~= "mips64";
79 	version(MIPS_O32) ret ~= "mips_o32";
80 	version(MIPS_N32) ret ~= "mips_n32";
81 	version(MIPS_O64) ret ~= "mips_o64";
82 	version(MIPS_N64) ret ~= "mips_n64";
83 	version(MIPS_EABI) ret ~= "mips_eabi";
84 	version(MIPS_NoFloat) ret ~= "mips_nofloat";
85 	version(MIPS_SoftFloat) ret ~= "mips_softfloat";
86 	version(MIPS_HardFloat) ret ~= "mips_hardfloat";
87 	version(SPARC) ret ~= "sparc";
88 	version(SPARC_V8Plus) ret ~= "sparc_v8plus";
89 	version(SPARC_SoftFP) ret ~= "sparc_softfp";
90 	version(SPARC_HardFP) ret ~= "sparc_hardfp";
91 	version(SPARC64) ret ~= "sparc64";
92 	version(S390) ret ~= "s390";
93 	version(S390X) ret ~= "s390x";
94 	version(HPPA) ret ~= "hppa";
95 	version(HPPA64) ret ~= "hppa64";
96 	version(SH) ret ~= "sh";
97 	version(SH64) ret ~= "sh64";
98 	version(Alpha) ret ~= "alpha";
99 	version(Alpha_SoftFP) ret ~= "alpha_softfp";
100 	version(Alpha_HardFP) ret ~= "alpha_hardfp";
101 	return ret;
102 };
103 
104 /// private
105 enum string compilerCheck = q{
106 	version(DigitalMars) return "dmd";
107 	else version(GNU) return "gdc";
108 	else version(LDC) return "ldc";
109 	else version(SDC) return "sdc";
110 	else return null;
111 };
112 
113 /** Determines the full build platform used for the current build.
114 
115 	Note that the `BuildPlatform.compilerBinary` field will be left empty.
116 
117 	See_Also: `determinePlatform`, `determineArchitecture`, `determineCompiler`
118 */
119 BuildPlatform determineBuildPlatform()
120 {
121 	BuildPlatform ret;
122 	ret.platform = determinePlatform();
123 	ret.architecture = determineArchitecture();
124 	ret.compiler = determineCompiler();
125 	ret.frontendVersion = __VERSION__;
126 	return ret;
127 }
128 
129 
130 /** Returns a list of platform identifiers that apply to the current
131 	build.
132 
133 	Example results are `["windows"]` or `["posix", "osx"]`. The identifiers
134 	correspond to the compiler defined version constants built into the
135 	language, except that they are converted to lower case.
136 
137 	See_Also: `determineBuildPlatform`
138 */
139 string[] determinePlatform()
140 {
141 	mixin(platformCheck);
142 }
143 
144 /** Returns a list of architecture identifiers that apply to the current
145 	build.
146 
147 	Example results are `["x86_64"]` or `["arm", "arm_softfloat"]`. The
148 	identifiers correspond to the compiler defined version constants built into
149 	the language, except that they are converted to lower case.
150 
151 	See_Also: `determineBuildPlatform`
152 */
153 string[] determineArchitecture()
154 {
155 	mixin(archCheck);
156 }
157 
158 /** Determines the canonical compiler name used for the current build.
159 
160 	The possible values currently are "dmd", "gdc", "ldc" or "sdc". If an
161 	unknown compiler is used, this function will return an empty string.
162 
163 	See_Also: `determineBuildPlatform`
164 */
165 string determineCompiler()
166 {
167 	mixin(compilerCheck);
168 }
169 
170 /** Matches a platform specification string against a build platform.
171 
172 	Specifications are build upon the following scheme, where each component
173 	is optional (indicated by []), but the order is obligatory:
174 	"[-platform][-architecture][-compiler]"
175 
176 	So the following strings are valid specifications: `"-windows-x86-dmd"`,
177 	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
178 
179 	Params:
180 		platform = The build platform to match agains the platform specification
181 	    specification = The specification being matched. It must either be an
182 	    	empty string or start with a dash.
183 
184 	Returns:
185 	    `true` if the given specification matches the build platform, `false`
186 	    otherwise. Using an empty string as the platform specification will
187 	    always result in a match.
188 */
189 bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
190 {
191 	import std.string : chompPrefix, format;
192 	import std.algorithm : canFind, splitter;
193 	import std.exception : enforce;
194 
195 	if (specification.empty) return true;
196 	if (platform == BuildPlatform.any) return true;
197 
198 	auto splitted = specification.chompPrefix("-").splitter('-');
199 	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
200 
201 	if (platform.platform.canFind(splitted.front)) {
202 		splitted.popFront();
203 		if (splitted.empty)
204 			return true;
205 	}
206 	if (platform.architecture.canFind(splitted.front)) {
207 		splitted.popFront();
208 		if (splitted.empty)
209 			return true;
210 	}
211 	if (platform.compiler == splitted.front) {
212 		splitted.popFront();
213 		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
214 		return true;
215 	}
216 	return false;
217 }
218 
219 ///
220 unittest {
221 	auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
222 	assert(platform.matchesSpecification(""));
223 	assert(platform.matchesSpecification("posix"));
224 	assert(platform.matchesSpecification("linux"));
225 	assert(platform.matchesSpecification("linux-dmd"));
226 	assert(platform.matchesSpecification("linux-x86_64-dmd"));
227 	assert(platform.matchesSpecification("x86_64"));
228 	assert(!platform.matchesSpecification("windows"));
229 	assert(!platform.matchesSpecification("ldc"));
230 	assert(!platform.matchesSpecification("windows-dmd"));
231 
232 	// Before PR#2279, a leading '-' was required
233 	assert(platform.matchesSpecification("-x86_64"));
234 }
235 
236 /// Represents a platform a package can be build upon.
237 struct BuildPlatform {
238 	/// Special constant used to denote matching any build platform.
239 	enum any = BuildPlatform(null, null, null, null, -1);
240 
241 	/// Platform identifiers, e.g. ["posix", "windows"]
242 	string[] platform;
243 	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
244 	string[] architecture;
245 	/// Canonical compiler name e.g. "dmd"
246 	string compiler;
247 	/// Compiler binary name e.g. "ldmd2"
248 	string compilerBinary;
249 	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
250 	int frontendVersion;
251 	/// Compiler version e.g. "1.11.0"
252 	string compilerVersion;
253 	/// Frontend version string from frontendVersion
254 	/// e.g: 2067 => "2.067"
255 	string frontendVersionString() const
256 	{
257 		import std.format : format;
258 
259 		const maj = frontendVersion / 1000;
260 		const min = frontendVersion % 1000;
261 		return format("%d.%03d", maj, min);
262 	}
263 	///
264 	unittest
265 	{
266 		BuildPlatform bp;
267 		bp.frontendVersion = 2067;
268 		assert(bp.frontendVersionString == "2.067");
269 	}
270 
271 	/// Checks to see if platform field contains windows
272 	bool isWindows() const {
273 		import std.algorithm : canFind;
274 		return this.platform.canFind("windows");
275 	}
276 	///
277 	unittest {
278 		BuildPlatform bp;
279 		bp.platform = ["windows"];
280 		assert(bp.isWindows);
281 		bp.platform = ["posix"];
282 		assert(!bp.isWindows);
283 	}
284 }