1 module dbuild.buildsystem;
2 
3 import dbuild.build;
4 import std.digest.md : MD5;
5 
6 /// context of a build
7 struct BuildContext
8 {
9     BuildDirs dirs;
10     BuildType type;
11     bool quiet;
12 }
13 
14 /// simple interface to a build system (such as CMake, or Autotools)
15 interface BuildSystem
16 {
17     /// Feeds the digest in a way that makes a unique build identifier.
18     void feedBuildId(ref MD5 digest);
19     /// Issue the commands to build the source code to the requested binaries.
20     void issueCmds(BuildContext ctx);
21 }
22 
23 /// Helper to create a CMAKE build system, with generator and options.
24 /// Option should not deal with install dir or build type, which
25 /// are set from the build context.
26 /// The build system will issue 3 commands: configure, build and install.
27 struct CMake
28 {
29     static CMake create(string generator=null, string[] options=null)
30     {
31         import dbuild.util : searchExecutable;
32         import std.exception : enforce;
33 
34         enforce(searchExecutable("cmake"), "Could not find CMake!");
35         CMake cmake;
36         cmake._gen = generator;
37         cmake._options = options;
38         if (generator == "Ninja") {
39             cmake._env["NINJA_STATUS"] = "[%r %f/%t %p %o/s] ";
40         }
41         return cmake;
42     }
43 
44     CMake withEnv(in string[string] env)
45     {
46         foreach(k, v; env) {
47             _env[k] = v;
48         }
49         return this;
50     }
51 
52     version(Windows)
53     CMake withMsvcSetup(int minVer=0, string[] vcvarsOptions=null)
54     {
55         import std.exception : enforce;
56 
57         enforce(tryMsvcSetup(minVer, vcvarsOptions), "Could not find suitable MSVC install");
58         return this;
59     }
60 
61     version(Windows)
62     bool tryMsvcSetup(int minVer=0, string[] vcvarsOptions=null)
63     {
64         import dbuild.msvc : detectMsvcInstalls, msvcEnvironment;
65         import dbuild.util : searchExecutable;
66 
67         if (searchExecutable("cl")) return true; // TODO: what version
68 
69         auto installs = detectMsvcInstalls();
70         if (!installs.length || installs[0].ver[0] < minVer) {
71             return false;
72         }
73         auto oldEnv = _env;
74         _env = msvcEnvironment(installs[0].vcvarsBat, vcvarsOptions);
75         foreach(k, v; oldEnv) {
76             _env[k] = v;
77         }
78         _additionalHashFeed = installs[0].vcvarsBat ~ vcvarsOptions;
79         _msvc = true;
80 
81         return true;
82     }
83 
84     private string findDefaultGenerator()
85     {
86         import dbuild.util : searchExecutable;
87 
88         version(Windows)
89         {
90             import dbuild.msvc : dubArchOptions;
91 
92             if (tryMsvcSetup(0, dubArchOptions())) {
93                 if (hasNinja) {
94                     return "Ninja";
95                 }
96                 else {
97                     return "NMake Makefiles";
98                 }
99             }
100         }
101         if (hasNinja) {
102             return "Ninja";
103         }
104         version(Windows) {
105             if (hasMingw32Make) {
106                 return "MinGW Makefiles";
107             }
108             else if (hasMake) { // under cygwin?
109                 return "MSYS Makefiles";
110             }
111         }
112         else {
113             if (hasMake) {
114                 return "Unix Makefiles";
115             }
116         }
117         return null;
118     }
119 
120     /// get the result BuildSystem
121     BuildSystem buildSystem()
122     {
123         import std.exception : enforce;
124 
125         if (!_gen) {
126             _gen = enforce(findDefaultGenerator(), "Could not find suitable CMake generator");
127         }
128 
129         version(Windows) {
130             if (_gen == "Ninja" && _msvc) {
131                 _env["CC"] = "cl";
132                 _env["CXX"] = "cl";
133             }
134         }
135 
136         return new CMakeBuildSystem(_gen, _options, _env, _additionalHashFeed);
137     }
138 
139     private string _gen;
140     private string[] _options;
141     private string[string] _env;
142     private string[] _additionalHashFeed;
143     version(Windows) private bool _msvc;
144 }
145 
146 
147 private @property bool hasNinja()
148 {
149     import dbuild.util : searchExecutable;
150     import std.concurrency : initOnce;
151 
152     static __gshared bool has;
153     return initOnce!has(searchExecutable("ninja") !is null);
154 }
155 
156 private @property bool hasGcc()
157 {
158     import dbuild.util : searchExecutable;
159     import std.concurrency : initOnce;
160 
161     static __gshared bool has;
162     return initOnce!has(searchExecutable("gcc") !is null);
163 }
164 
165 private @property bool hasClang()
166 {
167     import dbuild.util : searchExecutable;
168     import std.concurrency : initOnce;
169 
170     static __gshared bool has;
171     return initOnce!has(searchExecutable("clang") !is null);
172 }
173 
174 private @property bool hasMake()
175 {
176     import dbuild.util : searchExecutable;
177     import std.concurrency : initOnce;
178 
179     static __gshared bool has;
180     return initOnce!has(searchExecutable("make") !is null);
181 }
182 
183 private @property bool hasMingw32Make()
184 {
185     import dbuild.util : searchExecutable;
186     import std.concurrency : initOnce;
187 
188     static __gshared bool has;
189     return initOnce!has(searchExecutable("mingw32-make") !is null);
190 }
191 
192 
193 private class CMakeBuildSystem : BuildSystem
194 {
195     const(string) generator;
196     const(string[]) options;
197     string[string] env;
198     const(string[]) additionalHashFeed;
199 
200     this(in string generator, in string[] options, string[string] env, string[] additionalHashFeed)
201     {
202         this.generator = generator;
203         this.options = options;
204         this.env = env;
205         this.additionalHashFeed = additionalHashFeed;
206     }
207 
208     override void feedBuildId(ref MD5 digest)
209     {
210         import dbuild.util : feedDigestData;
211 
212         feedDigestData(digest, generator);
213         feedDigestData(digest, options);
214         if (additionalHashFeed.length) feedDigestData(digest, additionalHashFeed);
215     }
216 
217     override void issueCmds(BuildContext ctx)
218     {
219         import dbuild.util : runCommands;
220 
221         const string buildType = ctx.type == BuildType.deb ? "Debug" : "Release";
222 
223         const string[] configCmd = [
224             "cmake",
225             "-G", generator,
226             "-DCMAKE_BUILD_TYPE="~buildType,
227             "-DCMAKE_INSTALL_PREFIX="~ctx.dirs.installDir
228         ] ~ options ~ [
229             ctx.dirs.srcDir
230         ];
231         const string[] buildCmd = [
232             "cmake", "--build", "."
233         ];
234         const string[] installCmd = [
235             "cmake", "--build", ".", "--target", "install"
236         ];
237 
238         runCommands(
239             [ configCmd, buildCmd, installCmd ], ctx.dirs.buildDir, ctx.quiet, env
240         );
241     }
242 }