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 recipes.
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 	version(Emscripten) ret ~= "emscripten";
51 	return ret;
52 };
53 
54 /// private
55 enum string archCheck = q{
56 	string[] ret;
57 	version(X86) ret ~= "x86";
58 	// Hack: see #1535
59 	// Makes "x86_omf" available as a platform specifier in the package recipe
60 	version(X86) version(CRuntime_DigitalMars) ret ~= "x86_omf";
61 	// Hack: see #1059
62 	// When compiling with --arch=x86_mscoff build_platform.architecture is equal to ["x86"] and canFind below is false.
63 	// This hack prevents unnecessary warning 'Failed to apply the selected architecture x86_mscoff. Got ["x86"]'.
64 	// And also makes "x86_mscoff" available as a platform specifier in the package recipe
65 	version(X86) version(CRuntime_Microsoft) ret ~= "x86_mscoff";
66 	version(X86_64) ret ~= "x86_64";
67 	version(ARM) ret ~= "arm";
68 	version(AArch64) ret ~= "aarch64";
69 	version(ARM_Thumb) ret ~= "arm_thumb";
70 	version(ARM_SoftFloat) ret ~= "arm_softfloat";
71 	version(ARM_HardFloat) ret ~= "arm_hardfloat";
72 	version(PPC) ret ~= "ppc";
73 	version(PPC_SoftFP) ret ~= "ppc_softfp";
74 	version(PPC_HardFP) ret ~= "ppc_hardfp";
75 	version(PPC64) ret ~= "ppc64";
76 	version(IA64) ret ~= "ia64";
77 	version(MIPS) ret ~= "mips";
78 	version(MIPS32) ret ~= "mips32";
79 	version(MIPS64) ret ~= "mips64";
80 	version(MIPS_O32) ret ~= "mips_o32";
81 	version(MIPS_N32) ret ~= "mips_n32";
82 	version(MIPS_O64) ret ~= "mips_o64";
83 	version(MIPS_N64) ret ~= "mips_n64";
84 	version(MIPS_EABI) ret ~= "mips_eabi";
85 	version(MIPS_NoFloat) ret ~= "mips_nofloat";
86 	version(MIPS_SoftFloat) ret ~= "mips_softfloat";
87 	version(MIPS_HardFloat) ret ~= "mips_hardfloat";
88 	version(SPARC) ret ~= "sparc";
89 	version(SPARC_V8Plus) ret ~= "sparc_v8plus";
90 	version(SPARC_SoftFP) ret ~= "sparc_softfp";
91 	version(SPARC_HardFP) ret ~= "sparc_hardfp";
92 	version(SPARC64) ret ~= "sparc64";
93 	version(S390) ret ~= "s390";
94 	version(S390X) ret ~= "s390x";
95 	version(HPPA) ret ~= "hppa";
96 	version(HPPA64) ret ~= "hppa64";
97 	version(SH) ret ~= "sh";
98 	version(SH64) ret ~= "sh64";
99 	version(Alpha) ret ~= "alpha";
100 	version(Alpha_SoftFP) ret ~= "alpha_softfp";
101 	version(Alpha_HardFP) ret ~= "alpha_hardfp";
102 	version(LoongArch32) ret ~= "loongarch32";
103 	version(LoongArch64) ret ~= "loongarch64";
104 	version(LoongArch_SoftFloat) ret ~= "loongarch_softfloat";
105 	version(LoongArch_HardFloat) ret ~= "loongarch_hardfloat";
106 	version(WebAssembly) {
107 		version(D_LP64) ret ~= "wasm64";
108 		else ret ~= "wasm32";
109 	}
110 	return ret;
111 };
112 
113 /// private
114 enum string compilerCheck = q{
115 	version(DigitalMars) return "dmd";
116 	else version(GNU) return "gdc";
117 	else version(LDC) return "ldc";
118 	else version(SDC) return "sdc";
119 	else return null;
120 };
121 
122 /// private
123 enum string compilerCheckPragmas = q{
124 	version(DigitalMars) pragma(msg, ` "dmd"`);
125 	else version(GNU) pragma(msg, ` "gdc"`);
126 	else version(LDC) pragma(msg, ` "ldc"`);
127 	else version(SDC) pragma(msg, ` "sdc"`);
128 };
129 
130 /// private, converts the above appender strings to pragmas
131 string pragmaGen(string str) {
132 	import std.string : replace;
133 	return str.replace("return ret;", "").replace("string[] ret;", "").replace(`["`, `"`).replace(`", "`,`" "`).replace(`"]`, `"`).replace(`;`, "`);").replace("ret ~= ", "pragma(msg, ` ");
134 }
135 
136 
137 /** Determines the full build platform used for the current build.
138 
139 	Note that the `BuildPlatform.compilerBinary` field will be left empty.
140 
141 	See_Also: `determinePlatform`, `determineArchitecture`, `determineCompiler`
142 */
143 BuildPlatform determineBuildPlatform()
144 {
145 	BuildPlatform ret;
146 	ret.platform = determinePlatform();
147 	ret.architecture = determineArchitecture();
148 	ret.compiler = determineCompiler();
149 	ret.frontendVersion = __VERSION__;
150 	return ret;
151 }
152 
153 
154 /** Returns a list of platform identifiers that apply to the current
155 	build.
156 
157 	Example results are `["windows"]` or `["posix", "osx"]`. The identifiers
158 	correspond to the compiler defined version constants built into the
159 	language, except that they are converted to lower case.
160 
161 	See_Also: `determineBuildPlatform`
162 */
163 string[] determinePlatform()
164 {
165 	mixin(platformCheck);
166 }
167 
168 /** Returns a list of architecture identifiers that apply to the current
169 	build.
170 
171 	Example results are `["x86_64"]` or `["arm", "arm_softfloat"]`. The
172 	identifiers correspond to the compiler defined version constants built into
173 	the language, except that they are converted to lower case.
174 
175 	See_Also: `determineBuildPlatform`
176 */
177 string[] determineArchitecture()
178 {
179 	mixin(archCheck);
180 }
181 
182 /** Determines the canonical compiler name used for the current build.
183 
184 	The possible values currently are "dmd", "gdc", "ldc" or "sdc". If an
185 	unknown compiler is used, this function will return an empty string.
186 
187 	See_Also: `determineBuildPlatform`
188 */
189 string determineCompiler()
190 {
191 	mixin(compilerCheck);
192 }
193 
194 /** Matches a platform specification string against a build platform.
195 
196 	Specifications are build upon the following scheme, where each component
197 	is optional (indicated by []), but the order is obligatory:
198 	"[-platform][-architecture][-compiler]"
199 
200 	So the following strings are valid specifications: `"-windows-x86-dmd"`,
201 	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
202 
203 	Params:
204 		platform = The build platform to match against the platform specification
205 	    specification = The specification being matched. It must either be an
206 	    	empty string or start with a dash.
207 
208 	Returns:
209 	    `true` if the given specification matches the build platform, `false`
210 	    otherwise. Using an empty string as the platform specification will
211 	    always result in a match.
212 */
213 bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
214 {
215 	import std.string : chompPrefix, format;
216 	import std.algorithm : canFind, splitter;
217 	import std.exception : enforce;
218 
219 	if (specification.empty) return true;
220 	if (platform == BuildPlatform.any) return true;
221 
222 	auto splitted = specification.chompPrefix("-").splitter('-');
223 	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
224 
225 	if (platform.platform.canFind(splitted.front)) {
226 		splitted.popFront();
227 		if (splitted.empty)
228 			return true;
229 	}
230 	if (platform.architecture.canFind(splitted.front)) {
231 		splitted.popFront();
232 		if (splitted.empty)
233 			return true;
234 	}
235 	if (platform.compiler == splitted.front) {
236 		splitted.popFront();
237 		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
238 		return true;
239 	}
240 	return false;
241 }
242 
243 ///
244 unittest {
245 	auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
246 	assert(platform.matchesSpecification(""));
247 	assert(platform.matchesSpecification("posix"));
248 	assert(platform.matchesSpecification("linux"));
249 	assert(platform.matchesSpecification("linux-dmd"));
250 	assert(platform.matchesSpecification("linux-x86_64-dmd"));
251 	assert(platform.matchesSpecification("x86_64"));
252 	assert(!platform.matchesSpecification("windows"));
253 	assert(!platform.matchesSpecification("ldc"));
254 	assert(!platform.matchesSpecification("windows-dmd"));
255 
256 	// Before PR#2279, a leading '-' was required
257 	assert(platform.matchesSpecification("-x86_64"));
258 }
259 
260 /// Represents a platform a package can be build upon.
261 struct BuildPlatform {
262 	/// Special constant used to denote matching any build platform.
263 	enum any = BuildPlatform(null, null, null, null, -1);
264 
265 	/// Platform identifiers, e.g. ["posix", "windows"]
266 	string[] platform;
267 	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
268 	string[] architecture;
269 	/// Canonical compiler name e.g. "dmd"
270 	string compiler;
271 	/// Compiler binary name e.g. "ldmd2"
272 	string compilerBinary;
273 	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
274 	int frontendVersion;
275 	/// Compiler version e.g. "1.11.0"
276 	string compilerVersion;
277 	/// Frontend version string from frontendVersion
278 	/// e.g: 2067 => "2.067"
279 	string frontendVersionString() const
280 	{
281 		import std.format : format;
282 
283 		const maj = frontendVersion / 1000;
284 		const min = frontendVersion % 1000;
285 		return format("%d.%03d", maj, min);
286 	}
287 	///
288 	unittest
289 	{
290 		BuildPlatform bp;
291 		bp.frontendVersion = 2067;
292 		assert(bp.frontendVersionString == "2.067");
293 	}
294 
295 	/// Checks to see if platform field contains windows
296 	bool isWindows() const {
297 		import std.algorithm : canFind;
298 		return this.platform.canFind("windows");
299 	}
300 	///
301 	unittest {
302 		BuildPlatform bp;
303 		bp.platform = ["windows"];
304 		assert(bp.isWindows);
305 		bp.platform = ["posix"];
306 		assert(!bp.isWindows);
307 	}
308 
309 	/// Checks to see if platform field contains darwin
310 	bool isDarwin() const {
311 		import std.algorithm : canFind;
312 		return this.platform.canFind("darwin");
313 	}
314 	///
315 	unittest {
316 		BuildPlatform bp;
317 		bp.platform = ["osx", "darwin"];
318 		assert(bp.isDarwin);
319 		bp.platform = ["posix"];
320 		assert(!bp.isDarwin);
321 	}
322 }