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