1 module reggae.dub.info;
2 
3 import reggae.build;
4 import reggae.rules;
5 import reggae.types;
6 import reggae.sorting;
7 import reggae.options: Options;
8 import reggae.path: buildPath;
9 import std.algorithm: map, filter, find, splitter;
10 import std.array: array, join;
11 import std.range: chain;
12 
13 
14 enum TargetType {
15     autodetect,
16     none,
17     executable,
18     library,
19     sourceLibrary,
20     dynamicLibrary,
21     staticLibrary,
22     object,
23 }
24 
25 
26 struct DubPackage {
27     string name;
28     string path; /// path to the dub package
29     string mainSourceFile;
30     string targetFileName;
31     string[] dflags;
32     string[] lflags;
33     string[] importPaths;
34     string[] stringImportPaths;
35     string[] files;
36     TargetType targetType;
37     string[] versions;
38     string[] dependencies;
39     string[] libs;
40     string[] preBuildCommands;
41     string[] postBuildCommands;
42     string targetPath;
43 
44     string toString() @safe pure const {
45         import std.string: join;
46         import std.conv: to;
47         import std.traits: Unqual;
48 
49         auto ret = `DubPackage(`;
50         string[] lines;
51 
52         foreach(ref elt; this.tupleof) {
53             static if(is(Unqual!(typeof(elt)) == TargetType))
54                 lines ~= `TargetType.` ~ elt.to!string;
55             else static if(is(Unqual!(typeof(elt)) == string))
56                 lines ~= "`" ~ elt.to!string ~ "`";
57             else
58                 lines ~= elt.to!string;
59         }
60         ret ~= lines.join(`, `);
61         ret ~= `)`;
62         return ret;
63     }
64 
65     DubPackage dup() @safe pure nothrow const {
66         DubPackage ret;
67         foreach(i, member; this.tupleof) {
68             static if(__traits(compiles, member.dup))
69                 ret.tupleof[i] = member.dup;
70             else
71                 ret.tupleof[i] = member;
72         }
73         return ret;
74     }
75 }
76 
77 bool isStaticLibrary(in string fileName) @safe pure nothrow {
78     import std.path: extension;
79     version(Windows)
80         return fileName.extension == ".lib";
81     else
82         return fileName.extension == ".a";
83 }
84 
85 bool isObjectFile(in string fileName) @safe pure nothrow {
86     import reggae.rules.common: objExt;
87     import std.path: extension;
88     return fileName.extension == objExt;
89 }
90 
91 string inDubPackagePath(in string packagePath, in string filePath) @safe pure nothrow {
92     import std.algorithm: startsWith;
93     return filePath.startsWith("$project")
94         ? buildPath(filePath)
95         : buildPath(packagePath, filePath);
96 }
97 
98 struct DubObjsDir {
99     string globalDir;
100     string targetDir;
101 }
102 
103 struct DubInfo {
104 
105     import reggae.rules.dub: CompilationMode;
106 
107     DubPackage[] packages;
108 
109     DubInfo dup() @safe pure nothrow const {
110         import std.algorithm: map;
111         import std.array: array;
112         return DubInfo(packages.map!(a => a.dup).array);
113     }
114 
115     Target[] toTargets(in string[] compilerFlags = [],
116                        in CompilationMode compilationMode = CompilationMode.options,
117                        in DubObjsDir dubObjsDir = DubObjsDir(),
118                        in size_t startingIndex = 0)
119         @safe const
120     {
121         Target[] targets;
122 
123         foreach(i; startingIndex .. packages.length) {
124             targets ~= packageIndexToTargets(i, compilerFlags, compilationMode, dubObjsDir);
125         }
126 
127         return targets ~ allObjectFileSources ~ allStaticLibrarySources;
128     }
129 
130     // dubPackage[i] -> Target[]
131     private Target[] packageIndexToTargets(
132         in size_t dubPackageIndex,
133         in string[] compilerFlags = [],
134         in CompilationMode compilationMode = CompilationMode.options,
135         in DubObjsDir dubObjsDir = DubObjsDir())
136         @safe const
137     {
138         import reggae.path: deabsolutePath;
139         import reggae.config: options;
140         import std.range: chain, only;
141         import std.algorithm: filter;
142         import std.array: array, replace;
143         import std.functional: not;
144         import std.path: baseName, dirSeparator;
145         import std.string: stripRight;
146 
147         const dubPackage = packages[dubPackageIndex];
148         const importPaths = allImportPaths();
149         const stringImportPaths = dubPackage.allOf!(a => a.packagePaths(a.stringImportPaths))(packages);
150         const isMainPackage = dubPackageIndex == 0;
151         //the path must be explicit for the other packages, implicit for the "main"
152         //package
153         const projDir = isMainPackage ? "" : dubPackage.path;
154 
155         // -unittest should only apply to the main package
156         const(string)[] deUnitTest(in string[] flags) {
157             return isMainPackage
158                 ? flags
159                 : flags.filter!(f => f != "-unittest" && f != "-main").array;
160         }
161 
162         const flags = chain(dubPackage.dflags,
163                             dubPackage.versions.map!(a => "-version=" ~ a),
164                             options.dflags.splitter, // TODO: doesn't support quoted args with spaces
165                             deUnitTest(compilerFlags))
166             .array;
167 
168         const files = dubPackage.files
169             .filter!(not!isStaticLibrary)
170             .filter!(not!isObjectFile)
171             .map!(a => buildPath(dubPackage.path, a))
172             .array;
173 
174         auto compileFunc() {
175             final switch(compilationMode) with(CompilationMode) {
176                 case all: return &dlangObjectFilesTogether;
177                 case module_: return &dlangObjectFilesPerModule;
178                 case package_: return &dlangObjectFilesPerPackage;
179                 case options: return &dlangObjectFiles;
180             }
181         }
182 
183         auto targetsFunc() {
184             import reggae.rules.d: dlangStaticLibraryTogether;
185             import reggae.config: options;
186 
187             const isStaticLibDep =
188                 dubPackage.targetType == TargetType.staticLibrary &&
189                 dubPackageIndex != 0 &&
190                 !options.dubDepObjsInsteadOfStaticLib;
191 
192             return isStaticLibDep
193                 ? &dlangStaticLibraryTogether
194                 : compileFunc;
195         }
196 
197         auto packageTargets = targetsFunc()(files, flags, importPaths, stringImportPaths, [], projDir);
198 
199         // go through dub dependencies and adjust object file output paths
200         if(!isMainPackage) {
201             // optionally put the object files in dubObjsDir
202             if(dubObjsDir.globalDir != "") {
203                 foreach(ref target; packageTargets) {
204                     target.rawOutputs[0] = buildPath(dubObjsDir.globalDir,
205                                                     options.projectPath.deabsolutePath,
206                                                     dubObjsDir.targetDir,
207                                                     target.rawOutputs[0]);
208                 }
209             } else {
210                 const dubPkgRoot = buildPath(dubPackage.path).deabsolutePath.stripRight(dirSeparator);
211                 const shortenedRoot = buildPath("__dub__", baseName(dubPackage.path));
212                 foreach(ref target; packageTargets)
213                     target.rawOutputs[0] = buildPath(target.rawOutputs[0]).replace(dubPkgRoot, shortenedRoot);
214             }
215         }
216 
217         return packageTargets;
218     }
219 
220     Target[] packageNameToTargets(
221         in string name,
222         in string[] compilerFlags = [],
223         in CompilationMode compilationMode = CompilationMode.options,
224         in DubObjsDir dubObjsDir = DubObjsDir())
225         @safe const
226     {
227         foreach(const index, const dubPackage; packages) {
228             if(dubPackage.name == name)
229                 return packageIndexToTargets(index, compilerFlags, compilationMode, dubObjsDir);
230         }
231 
232         throw new Exception("Couldn't find package '" ~ name ~ "'");
233     }
234 
235     TargetName targetName() @safe const pure nothrow {
236         const fileName = packages[0].targetFileName;
237         return .targetName(targetType, fileName);
238     }
239 
240     string targetPath(in Options options) @safe const pure {
241         import std.path: relativePath;
242 
243         return options.workingDir == options.projectPath
244             ? packages[0].targetPath.relativePath(options.projectPath)
245             : "";
246     }
247 
248     TargetType targetType() @safe const pure nothrow {
249         return packages[0].targetType;
250     }
251 
252     string[] mainLinkerFlags() @safe pure nothrow const {
253         import std.array: join;
254 
255         const pack = packages[0];
256         return (pack.targetType == TargetType.library || pack.targetType == TargetType.staticLibrary)
257             ? ["-shared"]
258             : [];
259     }
260 
261     // template due to purity - in the 2nd build with the payload this is pure,
262     // but in the 1st build to generate the reggae executable it's not.
263     // See reggae.config.
264     string[] linkerFlags()() const {
265         import reggae.config: options;
266 
267         const allLibs = packages[0].libs;
268 
269         static string libFlag(in string lib) {
270             version(Posix)
271                 return "-L-l" ~ lib;
272             else
273                 return lib ~ ".lib";
274         }
275 
276         return
277             packages[0].libs.map!libFlag.array ~
278             packages[0].lflags.dup
279             ;
280     }
281 
282     string[] allImportPaths() @safe nothrow const {
283         import reggae.config: options;
284         import std.algorithm: sorted = sort, uniq;
285         import std.array: array;
286 
287         string[] paths;
288         auto rng = packages.map!(a => a.packagePaths(a.importPaths));
289         foreach(p; rng) paths ~= p;
290         auto allPaths = paths ~ options.projectPath;
291         return allPaths.sorted.uniq.array;
292     }
293 
294     // must be at the very end
295     private Target[] allStaticLibrarySources() @trusted /*join*/ nothrow const pure {
296         import std.algorithm: filter, map;
297         import std.array: array, join;
298 
299         return packages
300             .map!(a => cast(string[]) a.files.filter!isStaticLibrary.array)
301             .join
302             .map!(a => Target(a))
303             .array;
304     }
305 
306     private Target[] allObjectFileSources() @trusted nothrow const pure {
307         import std.algorithm.iteration: filter, map, uniq;
308         import std.algorithm.sorting: sort;
309         import std.array: array, join;
310 
311         string[] objectFiles =
312         packages
313             .map!(a => cast(string[]) a
314                   .files
315                   .filter!isObjectFile
316                   .map!(b => inDubPackagePath(a.path, b))
317                   .array
318             )
319             .join
320             .array;
321         sort(objectFiles);
322 
323         return objectFiles
324             .uniq
325             .map!(a => Target(a))
326             .array;
327     }
328 
329 
330     // all postBuildCommands in one shell command. Empty if there are none
331     string postBuildCommands() @safe pure nothrow const {
332         import std.string: join;
333         return packages[0].postBuildCommands.join(" && ");
334     }
335 }
336 
337 
338 private auto packagePaths(in DubPackage dubPackage, in string[] paths) @trusted nothrow {
339     return paths.map!(a => buildPath(dubPackage.path, a)).array;
340 }
341 
342 //@trusted because of map.array
343 private string[] allOf(alias F)(in DubPackage pack, in DubPackage[] packages) @trusted nothrow {
344 
345     import std.range: chain, only;
346     import std.array: array, front, empty;
347 
348     string[] result;
349 
350     foreach(dependency; chain(only(pack.name), pack.dependencies)) {
351         auto depPack = packages.find!(a => a.name == dependency);
352         if(!depPack.empty) {
353             result ~= F(depPack.front).array;
354         }
355     }
356     return result;
357 }
358 
359 
360 TargetName targetName(in TargetType targetType, in string fileName) @safe pure nothrow {
361 
362     import reggae.rules.common: exeExt;
363 
364     switch(targetType) with(TargetType) {
365     default:
366         return TargetName(fileName);
367 
368     case executable:
369         return TargetName(fileName ~ exeExt);
370 
371     case library:
372         version(Posix)
373             return TargetName("lib" ~ fileName ~ ".a");
374         else
375             return TargetName(fileName ~ ".lib");
376 
377     case dynamicLibrary:
378         version(Posix)
379             return TargetName("lib" ~ fileName ~ ".so");
380         else
381             return TargetName(fileName ~ ".dll");
382     }
383 }