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 : 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.splitter('-');
199 	assert(!splitted.empty, "No valid platform specification! The leading hyphen is required!");
200 	splitted.popFront(); // Drop leading empty match.
201 	enforce(!splitted.empty, format("Platform specification, if present, must not be empty: \"%s\"", specification));
202 
203 	if (platform.platform.canFind(splitted.front)) {
204 		splitted.popFront();
205 		if (splitted.empty)
206 			return true;
207 	}
208 	if (platform.architecture.canFind(splitted.front)) {
209 		splitted.popFront();
210 		if (splitted.empty)
211 			return true;
212 	}
213 	if (platform.compiler == splitted.front) {
214 		splitted.popFront();
215 		enforce(splitted.empty, "No valid specification! The compiler has to be the last element: " ~ specification);
216 		return true;
217 	}
218 	return false;
219 }
220 
221 ///
222 unittest {
223 	auto platform=BuildPlatform(["posix", "linux"], ["x86_64"], "dmd");
224 	assert(platform.matchesSpecification(""));
225 	assert(platform.matchesSpecification("-posix"));
226 	assert(platform.matchesSpecification("-linux"));
227 	assert(platform.matchesSpecification("-linux-dmd"));
228 	assert(platform.matchesSpecification("-linux-x86_64-dmd"));
229 	assert(platform.matchesSpecification("-x86_64"));
230 	assert(!platform.matchesSpecification("-windows"));
231 	assert(!platform.matchesSpecification("-ldc"));
232 	assert(!platform.matchesSpecification("-windows-dmd"));
233 }
234 
235 /// Represents a platform a package can be build upon.
236 struct BuildPlatform {
237 	/// Special constant used to denote matching any build platform.
238 	enum any = BuildPlatform(null, null, null, null, -1);
239 
240 	/// Platform identifiers, e.g. ["posix", "windows"]
241 	string[] platform;
242 	/// CPU architecture identifiers, e.g. ["x86", "x86_64"]
243 	string[] architecture;
244 	/// Canonical compiler name e.g. "dmd"
245 	string compiler;
246 	/// Compiler binary name e.g. "ldmd2"
247 	string compilerBinary;
248 	/// Compiled frontend version (e.g. `2067` for frontend versions 2.067.x)
249 	int frontendVersion;
250 	/// Compiler version e.g. "1.11.0"
251 	string compilerVersion;
252 	/// Frontend version string from frontendVersion
253 	/// e.g: 2067 => "2.067"
254 	string frontendVersionString() const
255 	{
256 		import std.format : format;
257 
258 		const maj = frontendVersion / 1000;
259 		const min = frontendVersion % 1000;
260 		return format("%d.%03d", maj, min);
261 	}
262 	///
263 	unittest
264 	{
265 		BuildPlatform bp;
266 		bp.frontendVersion = 2067;
267 		assert(bp.frontendVersionString == "2.067");
268 	}
269 }
270 
271