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 	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 unnecessary 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 	version(LoongArch32) ret ~= "loongarch32";
102 	version(LoongArch64) ret ~= "loongarch64";
103 	version(LoongArch_SoftFloat) ret ~= "loongarch_softfloat";
104 	version(LoongArch_HardFloat) ret ~= "loongarch_hardfloat";
105 	return ret;
106 };
107 
108 /// private
109 enum string compilerCheck = q{
110 	version(DigitalMars) return "dmd";
111 	else version(GNU) return "gdc";
112 	else version(LDC) return "ldc";
113 	else version(SDC) return "sdc";
114 	else return null;
115 };
116 
117 /// private
118 enum string compilerCheckPragmas = q{
119 	version(DigitalMars) pragma(msg, ` "dmd"`);
120 	else version(GNU) pragma(msg, ` "gdc"`);
121 	else version(LDC) pragma(msg, ` "ldc"`);
122 	else version(SDC) pragma(msg, ` "sdc"`);
123 };
124 
125 /// private, converts the above appender strings to pragmas
126 string pragmaGen(string str) {
127 	import std.string : replace;
128 	return str.replace("return ret;", "").replace("string[] ret;", "").replace(`["`, `"`).replace(`", "`,`" "`).replace(`"]`, `"`).replace(`;`, "`);").replace("ret ~= ", "pragma(msg, ` ");
129 }
130 
131 
132 /** Determines the full build platform used for the current build.
133 
134 	Note that the `BuildPlatform.compilerBinary` field will be left empty.
135 
136 	See_Also: `determinePlatform`, `determineArchitecture`, `determineCompiler`
137 */
138 BuildPlatform determineBuildPlatform()
139 {
140 	BuildPlatform ret;
141 	ret.platform = determinePlatform();
142 	ret.architecture = determineArchitecture();
143 	ret.compiler = determineCompiler();
144 	ret.frontendVersion = __VERSION__;
145 	return ret;
146 }
147 
148 
149 /** Returns a list of platform identifiers that apply to the current
150 	build.
151 
152 	Example results are `["windows"]` or `["posix", "osx"]`. The identifiers
153 	correspond to the compiler defined version constants built into the
154 	language, except that they are converted to lower case.
155 
156 	See_Also: `determineBuildPlatform`
157 */
158 string[] determinePlatform()
159 {
160 	mixin(platformCheck);
161 }
162 
163 /** Returns a list of architecture identifiers that apply to the current
164 	build.
165 
166 	Example results are `["x86_64"]` or `["arm", "arm_softfloat"]`. The
167 	identifiers correspond to the compiler defined version constants built into
168 	the language, except that they are converted to lower case.
169 
170 	See_Also: `determineBuildPlatform`
171 */
172 string[] determineArchitecture()
173 {
174 	mixin(archCheck);
175 }
176 
177 /** Determines the canonical compiler name used for the current build.
178 
179 	The possible values currently are "dmd", "gdc", "ldc" or "sdc". If an
180 	unknown compiler is used, this function will return an empty string.
181 
182 	See_Also: `determineBuildPlatform`
183 */
184 string determineCompiler()
185 {
186 	mixin(compilerCheck);
187 }
188 
189 /** Matches a platform specification string against a build platform.
190 
191 	Specifications are build upon the following scheme, where each component
192 	is optional (indicated by []), but the order is obligatory:
193 	"[-platform][-architecture][-compiler]"
194 
195 	So the following strings are valid specifications: `"-windows-x86-dmd"`,
196 	`"-dmd"`, `"-arm"`, `"-arm-dmd"`, `"-windows-dmd"`
197 
198 	Params:
199 		platform = The build platform to match against the platform specification
200 	    specification = The specification being matched. It must either be an
201 	    	empty string or start with a dash.
202 
203 	Returns:
204 	    `true` if the given specification matches the build platform, `false`
205 	    otherwise. Using an empty string as the platform specification will
206 	    always result in a match.
207 */
208 bool matchesSpecification(in BuildPlatform platform, const(char)[] specification)
209 {
210 	import std.string : chompPrefix, format;
211 	import std.algorithm : canFind, splitter;
212 	import std.exception : enforce;
213 
214 	if (specification.empty) return true;
215 	if (platform == BuildPlatform.any) return true;
216 
217 	auto splitted = specification.chompPrefix("-").splitter('-');
218 	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
219 
220 	if (platform.platform.canFind(splitted.front)) {
221 		splitted.popFront();
222 		if (splitted.empty)
223 			return true;
224 	}
225 	if (platform.architecture.canFind(splitted.front)) {
226 		splitted.popFront();
227 		if (splitted.empty)
228 			return true;
229 	}
230 	if (platform.compiler == splitted.front) {
231 		splitted.popFront();
232 		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
233 		return true;
234 	}
235 	return false;
236 }
237 
238 ///
239 unittest {
240 	auto platform = BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
241 	assert(platform.matchesSpecification(""));
242 	assert(platform.matchesSpecification("posix"));
243 	assert(platform.matchesSpecification("linux"));
244 	assert(platform.matchesSpecification("linux-dmd"));
245 	assert(platform.matchesSpecification("linux-x86_64-dmd"));
246 	assert(platform.matchesSpecification("x86_64"));
247 	assert(!platform.matchesSpecification("windows"));
248 	assert(!platform.matchesSpecification("ldc"));
249 	assert(!platform.matchesSpecification("windows-dmd"));
250 
251 	// Before PR#2279, a leading '-' was required
252 	assert(platform.matchesSpecification("-x86_64"));
253 }
254 
255 /// Represents a platform a package can be build upon.
256 struct BuildPlatform {
257 	/// Special constant used to denote matching any build platform.
258 	enum any = BuildPlatform(null, null, null, null, -1);
259 
260 	/// Platform identifiers, e.g. ["posix", "windows"]
261 	string[] platform;
262 	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
263 	string[] architecture;
264 	/// Canonical compiler name e.g. "dmd"
265 	string compiler;
266 	/// Compiler binary name e.g. "ldmd2"
267 	string compilerBinary;
268 	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
269 	int frontendVersion;
270 	/// Compiler version e.g. "1.11.0"
271 	string compilerVersion;
272 	/// Frontend version string from frontendVersion
273 	/// e.g: 2067 => "2.067"
274 	string frontendVersionString() const
275 	{
276 		import std.format : format;
277 
278 		const maj = frontendVersion / 1000;
279 		const min = frontendVersion % 1000;
280 		return format("%d.%03d", maj, min);
281 	}
282 	///
283 	unittest
284 	{
285 		BuildPlatform bp;
286 		bp.frontendVersion = 2067;
287 		assert(bp.frontendVersionString == "2.067");
288 	}
289 
290 	/// Checks to see if platform field contains windows
291 	bool isWindows() const {
292 		import std.algorithm : canFind;
293 		return this.platform.canFind("windows");
294 	}
295 	///
296 	unittest {
297 		BuildPlatform bp;
298 		bp.platform = ["windows"];
299 		assert(bp.isWindows);
300 		bp.platform = ["posix"];
301 		assert(!bp.isWindows);
302 	}
303 }