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