1 module mage.msbuild.cpp;
2 
3 import mage;
4 import mage.msbuild : VSInfo, trPlatform;
5 import mage.msbuild.clcompile;
6 import mage.msbuild.link;
7 import mage.util.option;
8 
9 import std.typetuple : allSatisfy;
10 import std.uuid;
11 
12 
13 struct MSBuildProject
14 {
15   string name;
16   UUID guid;
17   string toolsVersion;
18   MSBuildConfig[] configs;
19   Path[] headers;
20   Path[] cpps;
21   Path[] otherFiles;
22   Target target;
23 
24   /// Decides whether this project is the first to show up in the sln file.
25   bool isStartup = false;
26 
27   @disable this();
28 
29   this(string name)
30   {
31     this.name = name;
32     this.guid = randomUUID();
33   }
34 }
35 
36 struct MSBuildConfig
37 {
38   string name;
39   string architecture;
40   string type;
41   Path outputFile;
42   Path intermediatesDir;
43   Option!bool useDebugLibs;
44   string platformToolset;
45   string characterSet;
46   Option!bool wholeProgramOptimization;
47   Option!bool linkIncremental;
48   ClCompile clCompile;
49   Link link;
50   Path[] headerFiles;
51   Path[] compilationUnits;
52 }
53 
54 
55 string trWarningLevel(int level) {
56   return "Level %s".format(level);
57 }
58 
59 string trOptimization(int level) {
60   try {
61     return [ "Disabled", "MinSize", "MaxSpeed", "Full" ][level];
62   }
63   catch(core.exception.RangeError) {
64     log.warning("Unsupported warning level '%'".format(level));
65   }
66   return null;
67 }
68 
69 string trType2FileExt(string type)
70 {
71   try {
72     return [ "Application" : ".exe", "StaticLibrary" : ".lib" ][type];
73   }
74   catch(core.exception.RangeError) {
75     log.warning(`Unsupported config type "%s"`.format(type));
76   }
77   return null;
78 }
79 
80 
81 MSBuildProject createProject(MagicContext context, ref in VSInfo info, Target target)
82 {
83   string name;
84   {
85     auto varName = target["name"];
86     if(!varName.hasValue())
87     {
88       log.error("Target must have a `name' property!");
89       assert(0);
90     }
91     name = varName.get!string();
92   }
93 
94   auto _= log.Block(`Create Cpp MSBuildProject for "%s"`, name);
95 
96   auto localDefaults = Properties("%s_defaults".format(name));
97   localDefaults["outputDir"] = Path("$(SolutionDir)$(Platform)$(Configuration)");
98   localDefaults["characterSet"] = "Unicode";
99 
100   target.properties.prettyPrint();
101 
102   auto projEnv = Environment("%s_proj_env".format(name), target.properties, *G.env[0], localDefaults, *G.env[1]);
103   context.setActive(projEnv);
104   scope(exit) context.popActive();
105 
106   auto proj = MSBuildProject(name);
107   if(auto var = target.tryGet("isStartup")) {
108     proj.isStartup = var.get!bool;
109   }
110   proj.target = target;
111   proj.toolsVersion = info.toolsVersion;
112 
113   auto cfgs = projEnv.first("configurations")
114                      .enforce("No `configurations' found.");
115   foreach(ref Config cfg; *cfgs)
116   {
117     proj.configs.length += 1;
118     auto projCfg = &proj.configs[$-1];
119 
120     import std.traits;
121     pragma(msg, fullyQualifiedName!(typeof(cfg)));
122     auto env = Environment(projEnv.name ~ "_cfg", cfg.properties, projEnv.env);
123     context.setActive(env);
124     scope(exit) context.popActive();
125 
126     Properties fallback;
127     auto fallbackEnv = Environment(env.name ~ "_fallback", fallback);
128     fallbackEnv["characterSet"] = "Unicode";
129     fallbackEnv["wholeProgramOptimization"] = false;
130     fallbackEnv["intermediatesDir"] = Path("$(SolutionDir)temp/$(TargetName)_$(Platform)_$(Configuration)");
131     fallbackEnv["linkIncremental"] = false;
132     env.internal = &fallbackEnv;
133 
134     projCfg.name = env.configName();
135     log.info("Configuration: %s".format(projCfg.name));
136     fallback.name = "%s_fallback".format(projCfg.name);
137 
138     projCfg.architecture = env.configArchitecture();
139     log.info("Architecture: %s".format(projCfg.architecture));
140 
141     projCfg.type = env.configType();
142     projCfg.useDebugLibs = env.configUseDebugLibgs(projCfg.name);
143     projCfg.platformToolset = env.configPlatformToolset(info);
144     projCfg.characterSet = env.configCharacterSet();
145     projCfg.wholeProgramOptimization = env.configWholeProgramOptimization();
146     projCfg.outputFile = env.configOutputFile(proj, *projCfg);
147     projCfg.intermediatesDir = env.configIntermediatesDir();
148     projCfg.linkIncremental = env.configLinkIncremental();
149     env.configFiles(projCfg.headerFiles, projCfg.compilationUnits);
150 
151     sanitize(*projCfg);
152 
153     projCfg.clCompile = createClCompile(*projCfg, env);
154     projCfg.link = createLink(*projCfg, info, env);
155   }
156 
157   return proj;
158 }
159 
160 
161 private auto required(ref Environment env, string propName)
162 {
163   return env.first(propName).enforce("Missing required property `%s'.".format(propName));
164 }
165 
166 private auto optional(ref Environment env, string propName)
167 {
168   auto pVar = env.first(propName);
169   if(pVar is null || !pVar.hasValue()) {
170     log.trace("Missing optional property `%s'.", propName);
171     assert(env.internal, "Missing fallback environment.");
172     pVar = env.internal.first(propName);
173     assert(pVar, "Missing fallback value.");
174   }
175   return pVar;
176 }
177 
178 
179 string configName(ref Environment env)
180 {
181   return env.required("name")
182             .get!string;
183 }
184 
185 string configArchitecture(ref Environment env)
186 {
187   auto arch = env.required("architecture");
188   return trPlatform(arch.get!string);
189 }
190 
191 string configType(ref Environment env)
192 {
193   auto type = env.required("type")
194                  .get!string;
195   switch(type) {
196     case "executable": return "Application";
197     case "library":
198     {
199       auto libType = env.required("libType")
200                         .get!LibraryType;
201       final switch(libType)
202       {
203         case LibraryType.Static: return "StaticLibrary";
204         case LibraryType.Shared: assert(0, "Not implemented.");
205       }
206     }
207     default: break;
208   }
209 
210   assert(0, "Unknown config type: %s".format(type));
211 }
212 
213 string configCharacterSet(ref Environment env)
214 {
215   auto varCharset = env.optional("characterSet");
216   // TODO Check for correct values.
217   return varCharset.get!string;
218 }
219 
220 bool configWholeProgramOptimization(ref Environment env)
221 {
222   auto varValue = env.optional("wholeProgramOptimization");
223   return varValue.get!bool;
224 }
225 
226 Path configOutputFile(ref Environment env, ref MSBuildProject proj, ref MSBuildConfig cfg)
227 {
228   Path path;
229 
230   auto pVar = env.first("outputFile");
231   if(pVar)
232   {
233     path = pVar.get!Path;
234   }
235   else
236   {
237     pVar = env.first("outputDir")
238               .enforce("Neither `outputFile' not `outputDir' was found, "
239                        "but need at least one of them.");
240     path = absolute(pVar.get!Path() ~ (proj.name ~ trType2FileExt(cfg.type)), env["mageFilePath"].get!Path.parent);
241   }
242 
243   return path;
244 }
245 
246 Path configIntermediatesDir(ref Environment env)
247 {
248   auto pVar = env.optional("intermediatesDir");
249   return pVar.get!Path.absolute(env["mageFilePath"].get!Path.parent);
250 }
251 
252 bool configLinkIncremental(ref Environment env)
253 {
254   auto pVar = env.optional("linkIncremental");
255   return pVar.get!bool;
256 }
257 
258 /// Params:
259 ///   cfgName = If `env' does not contain the property "useDebugLibs",
260 ///             and this argument contains the string "debug" (ignoring the case),
261 ///             this function will yield $(D true).
262 bool configUseDebugLibgs(ref Environment env, string cfgName)
263 {
264   auto pVar = env.first("useDebugLibs");
265   if(pVar) {
266     return pVar.get!bool;
267   }
268 
269   bool isRelease = cfgName.canFind!((a, b) => a.toLower() == b.toLower())("release");
270   return !isRelease;
271 }
272 
273 string configPlatformToolset(ref Environment env, ref in VSInfo info)
274 {
275   auto pVar = env.first("platformToolset");
276   if(pVar) {
277     return pVar.get!string;
278   }
279   return info.platformToolset;
280 }
281 
282 void configFiles(ref Environment env, ref Path[] headerFiles, ref Path[] compilationUnits)
283 {
284   auto files = env.required("sourceFiles").get!(Path[]);
285   auto mageFilePath = env.required("mageFilePath").get!Path;
286   auto filesRoot = mageFilePath.parent;
287   foreach(ref file; files)
288   {
289     auto _block = log.Block(`Processing file "%s"`, file);
290     file = file.absolute(filesRoot);
291 
292     auto ext = file.extension;
293     if(ext == ".h" || ext == ".hpp")
294     {
295       headerFiles ~= file;
296     }
297     else if(ext == ".cpp" || ext == ".cxx" || ext == ".c")
298     {
299       compilationUnits ~= file;
300     }
301     else
302     {
303       log.warning(`Unknown file extension: %s`, ext);
304     }
305   }
306 }
307 
308 void sanitize(ref MSBuildConfig cfg)
309 {
310   auto useDebugLibs = cfg.useDebugLibs && cfg.useDebugLibs.unwrap();
311   auto wholeProgramOptimization = cfg.wholeProgramOptimization && cfg.wholeProgramOptimization.unwrap();
312 
313   if(useDebugLibs && wholeProgramOptimization) {
314     log.trace(`When using debug libs, the option "wholeProgramOptimization" ` ~
315               `cannot be set. Visual Studio itself forbids that. Ignoring it for now.`);
316     cfg.wholeProgramOptimization = false;
317   }
318 
319   // TODO Check which options are not compatible with linkIncremental in visual studio.
320 
321   // TODO More.
322 }