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         const sharedFlag = targetType == TargetType.dynamicLibrary ? ["-fPIC"] : [];
156 
157         // -unittest should only apply to the main package
158         const(string)[] deUnitTest(in string[] flags) {
159             return isMainPackage
160                 ? flags
161                 : flags.filter!(f => f != "-unittest" && f != "-main").array;
162         }
163 
164         const flags = chain(dubPackage.dflags,
165                             dubPackage.versions.map!(a => "-version=" ~ a),
166                             options.dflags.splitter, // TODO: doesn't support quoted args with spaces
167                             sharedFlag,
168                             deUnitTest(compilerFlags))
169             .array;
170 
171         const files = dubPackage.files
172             .filter!(not!isStaticLibrary)
173             .filter!(not!isObjectFile)
174             .map!(a => buildPath(dubPackage.path, a))
175             .array;
176 
177         auto compileFunc() {
178             final switch(compilationMode) with(CompilationMode) {
179                 case all: return &dlangObjectFilesTogether;
180                 case module_: return &dlangObjectFilesPerModule;
181                 case package_: return &dlangObjectFilesPerPackage;
182                 case options: return &dlangObjectFiles;
183             }
184         }
185 
186         auto targetsFunc() {
187             import reggae.rules.d: dlangStaticLibraryTogether;
188             import reggae.config: options;
189 
190             const isStaticLibDep =
191                 dubPackage.targetType == TargetType.staticLibrary &&
192                 dubPackageIndex != 0 &&
193                 !options.dubDepObjsInsteadOfStaticLib;
194 
195             return isStaticLibDep
196                 ? &dlangStaticLibraryTogether
197                 : compileFunc;
198         }
199 
200         auto packageTargets = targetsFunc()(files, flags, importPaths, stringImportPaths, [], projDir);
201 
202         // go through dub dependencies and adjust object file output paths
203         if(!isMainPackage) {
204             // optionally put the object files in dubObjsDir
205             if(dubObjsDir.globalDir != "") {
206                 foreach(ref target; packageTargets) {
207                     target.rawOutputs[0] = buildPath(dubObjsDir.globalDir,
208                                                     options.projectPath.deabsolutePath,
209                                                     dubObjsDir.targetDir,
210                                                     target.rawOutputs[0]);
211                 }
212             } else {
213                 const dubPkgRoot = buildPath(dubPackage.path).deabsolutePath.stripRight(dirSeparator);
214                 const shortenedRoot = buildPath("__dub__", baseName(dubPackage.path));
215                 foreach(ref target; packageTargets)
216                     target.rawOutputs[0] = buildPath(target.rawOutputs[0]).replace(dubPkgRoot, shortenedRoot);
217             }
218         }
219 
220         return packageTargets;
221     }
222 
223     Target[] packageNameToTargets(
224         in string name,
225         in string[] compilerFlags = [],
226         in CompilationMode compilationMode = CompilationMode.options,
227         in DubObjsDir dubObjsDir = DubObjsDir())
228         @safe const
229     {
230         foreach(const index, const dubPackage; packages) {
231             if(dubPackage.name == name)
232                 return packageIndexToTargets(index, compilerFlags, compilationMode, dubObjsDir);
233         }
234 
235         throw new Exception("Couldn't find package '" ~ name ~ "'");
236     }
237 
238     TargetName targetName() @safe const pure nothrow {
239         const fileName = packages[0].targetFileName;
240         return .targetName(targetType, fileName);
241     }
242 
243     string targetPath(in Options options) @safe const pure {
244         import std.path: relativePath;
245 
246         return options.workingDir == options.projectPath
247             ? packages[0].targetPath.relativePath(options.projectPath)
248             : "";
249     }
250 
251     TargetType targetType() @safe const pure nothrow {
252         return packages[0].targetType;
253     }
254 
255     string[] mainLinkerFlags() @safe pure nothrow const {
256         import std.array: join;
257 
258         const pack = packages[0];
259         return (pack.targetType == TargetType.library || pack.targetType == TargetType.staticLibrary)
260             ? ["-shared"]
261             : [];
262     }
263 
264     // template due to purity - in the 2nd build with the payload this is pure,
265     // but in the 1st build to generate the reggae executable it's not.
266     // See reggae.config.
267     string[] linkerFlags()() const {
268         import reggae.config: options;
269 
270         const allLibs = packages[0].libs;
271 
272         static string libFlag(in string lib) {
273             version(Posix)
274                 return "-L-l" ~ lib;
275             else
276                 return lib ~ ".lib";
277         }
278 
279         return
280             packages[0].libs.map!libFlag.array ~
281             packages[0].lflags.dup
282             ;
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 /*join*/ nothrow const pure {
299         import std.algorithm: filter, map;
300         import std.array: array, join;
301 
302         return packages
303             .map!(a => cast(string[]) a.files.filter!isStaticLibrary.array)
304             .join
305             .map!(a => Target(a))
306             .array;
307     }
308 
309     private Target[] allObjectFileSources() @trusted nothrow const pure {
310         import std.algorithm.iteration: filter, map, uniq;
311         import std.algorithm.sorting: sort;
312         import std.array: array, join;
313 
314         string[] objectFiles =
315         packages
316             .map!(a => cast(string[]) a
317                   .files
318                   .filter!isObjectFile
319                   .map!(b => inDubPackagePath(a.path, b))
320                   .array
321             )
322             .join
323             .array;
324         sort(objectFiles);
325 
326         return objectFiles
327             .uniq
328             .map!(a => Target(a))
329             .array;
330     }
331 
332 
333     // all postBuildCommands in one shell command. Empty if there are none
334     string postBuildCommands() @safe pure nothrow const {
335         import std..string: join;
336         return packages[0].postBuildCommands.join(" && ");
337     }
338 }
339 
340 
341 private auto packagePaths(in DubPackage dubPackage, in string[] paths) @trusted nothrow {
342     return paths.map!(a => buildPath(dubPackage.path, a)).array;
343 }
344 
345 //@trusted because of map.array
346 private string[] allOf(alias F)(in DubPackage pack, in DubPackage[] packages) @trusted nothrow {
347 
348     import std.range: chain, only;
349     import std.array: array, front, empty;
350 
351     string[] result;
352 
353     foreach(dependency; chain(only(pack.name), pack.dependencies)) {
354         auto depPack = packages.find!(a => a.name == dependency);
355         if(!depPack.empty) {
356             result ~= F(depPack.front).array;
357         }
358     }
359     return result;
360 }
361 
362 
363 TargetName targetName(in TargetType targetType, in string fileName) @safe pure nothrow {
364 
365     import reggae.rules.common: exeExt;
366 
367     switch(targetType) with(TargetType) {
368     default:
369         return TargetName(fileName);
370 
371     case executable:
372         return TargetName(fileName ~ exeExt);
373 
374     case library:
375         version(Posix)
376             return TargetName("lib" ~ fileName ~ ".a");
377         else
378             return TargetName(fileName ~ ".lib");
379 
380     case dynamicLibrary:
381         version(Posix)
382             return TargetName("lib" ~ fileName ~ ".so");
383         else
384             return TargetName(fileName ~ ".dll");
385     }
386 }