1 module reggae.rules.common;
2 
3 
4 import reggae.build;
5 import reggae.ctaa;
6 import reggae.types;
7 import std.algorithm;
8 import std.path;
9 import std.array: array;
10 import std.traits;
11 import std.typecons;
12 
13 /**
14  This template function exists so as to be referenced in a reggaefile.d
15  at top-level without being called via $(D alias). That way it can be
16  named and used in a further $(D Target) definition without the need to
17  define a function returning $(D Build).
18  This function gets the source files to be compiled at runtime by searching
19  for source files in the given directories, adding files and filtering
20  as appropriate by the parameters given in $(D sources), its first compile-time
21  parameter. The other parameters are self-explanatory.
22 
23  This function returns a list of targets that are the result of compiling
24  source files written in the supported languages. The $(Sources) function
25  can be used to specify source directories and source files, as well as
26  a filter function to select those files that are actually wanted.
27  */
28 Target[] objectFiles(alias sourcesFunc = Sources!(),
29                      Flags flags = Flags(),
30                      ImportPaths includes = ImportPaths(),
31                      StringImportPaths stringImports = StringImportPaths(),
32     )() @trusted {
33 
34     import std.stdio;
35     const srcFiles = sourcesToFileNames!(sourcesFunc);
36     return srcFilesToObjectTargets(srcFiles, flags, includes, stringImports);
37 }
38 
39 
40 /**
41  An object file, typically from one source file in a certain language
42  (although for D the default is a whole package). The language is determined
43  by the file extension of the file passed in.
44  The $(D projDir) variable is best left alone; right now only the dub targets
45  make use of it (since dub packages are by definition outside of the project
46  source tree).
47 */
48 Target objectFile(in SourceFile srcFile,
49                   in Flags flags = Flags(),
50                   in ImportPaths includePaths = ImportPaths(),
51                   in StringImportPaths stringImportPaths = StringImportPaths(),
52                   Target[] implicits = [],
53                   in string projDir = "$project") @safe pure {
54 
55     auto cmd = compileCommand(srcFile.value, flags.value, includePaths.value, stringImportPaths.value, projDir);
56     return Target(srcFile.value.objFileName, cmd, [Target(srcFile.value)], implicits);
57 }
58 
59 /**
60  A binary executable. The same as calling objectFiles and link
61  with these parameters.
62  */
63 Target executable(ExeName exeName,
64                   alias sourcesFunc = Sources!(),
65                   Flags compilerFlags = Flags(),
66                   ImportPaths includes = ImportPaths(),
67                   StringImportPaths stringImports = StringImportPaths(),
68                   Flags linkerFlags = Flags())
69     () {
70     auto objs = objectFiles!(sourcesFunc, compilerFlags, includes, stringImports);
71     return link!(exeName, { return objs; }, linkerFlags);
72 }
73 
74 Target executable(in string projectPath,
75                   in string name,
76                   in string[] srcDirs,
77                   in string[] excDirs,
78                   in string[] srcFiles,
79                   in string[] excFiles,
80                   in string compilerFlags,
81                   in string linkerFlags,
82                   in string[] includes,
83                   in string[] stringImports)
84 {
85     auto objs = objectFiles(projectPath, srcDirs, excDirs, srcFiles, excFiles, compilerFlags, includes, stringImports);
86     return link(ExeName(name), objs, Flags(linkerFlags));
87 }
88 
89 
90 
91 /**
92  "Compile-time" link function.
93  Its parameters are compile-time so that it can be aliased and used
94  at global scope in a reggafile.
95  Links an executable from the given dependency targets. The linker used
96  depends on the file extension of the leaf nodes of the passed-in targets.
97  If any D files are found, the linker is the D compiler, and so on with
98  C++ and C. If none of those apply, the D compiler is used.
99  */
100 Target link(ExeName exeName, alias dependenciesFunc, Flags flags = Flags())() {
101     auto dependencies = dependenciesFunc();
102     return link(exeName, dependencies, flags);
103 }
104 
105 /**
106  Regular run-time link function.
107  Links an executable from the given dependency targets. The linker used
108  depends on the file extension of the leaf nodes of the passed-in targets.
109  If any D files are found, the linker is the D compiler, and so on with
110  C++ and C. If none of those apply, the D compiler is used.
111  */
112 Target link(in ExeName exeName, Target[] dependencies, in Flags flags = Flags()) @safe pure {
113     auto command = Command(CommandType.link,
114                            assocList([assocEntry("flags", flags.value.splitter.array)]));
115     return Target(exeName.value, command, dependencies);
116 }
117 
118 
119 /**
120  Convenience rule for creating static libraries
121  */
122 Target[] staticLibrary(string name,
123                        alias sourcesFunc = Sources!(),
124                        Flags flags = Flags(),
125                        ImportPaths includes = ImportPaths(),
126                        StringImportPaths stringImports = StringImportPaths(),
127                        alias dependenciesFunc = () { Target[] ts; return ts; })
128     ()
129 {
130 
131     version(Posix)
132         return staticLibraryTarget(name, objectFiles!(sourcesFunc, flags, includes, stringImports)() ~ dependenciesFunc());
133     else
134         throw new Exception("Can only create static libraries on Posix");
135 }
136 
137 /**
138  "Compile-time" target creation.
139  Its parameters are compile-time so that it can be aliased and used
140  at global scope in a reggaefile
141  */
142 Target target(alias outputs,
143               alias command = "",
144               alias dependenciesFunc = () { Target[] ts; return ts; },
145               alias implicitsFunc = () { Target[] ts; return ts; })() @trusted {
146 
147     auto depsRes = dependenciesFunc();
148     auto impsRes = implicitsFunc();
149 
150     static if(isArray!(typeof(depsRes)))
151         auto dependencies = depsRes;
152     else
153         auto dependencies = [depsRes];
154 
155     static if(isArray!(typeof(impsRes)))
156         auto implicits = impsRes;
157     else
158         auto implicits = [impsRes];
159 
160 
161     return Target(outputs, command, dependencies, implicits);
162 }
163 
164 
165 /**
166  * Convenience alias for appending targets without calling any runtime function.
167  * This replaces the need to manually define a function to return a `Build` struct
168  * just to concatenate targets
169  */
170 Target[] targetConcat(T...)() {
171     Target[] ret;
172     foreach(target; T) {
173         static if(isCallable!target)
174             ret ~= target();
175         else
176             ret ~= target;
177     }
178     return ret;
179 }
180 
181 /**
182  Compile-time version of Target.phony
183  */
184 Target phony(string name,
185              string shellCommand,
186              alias dependenciesFunc = () { Target[] ts; return ts; },
187              alias implicitsFunc = () { Target[] ts; return ts; })() {
188     return Target.phony(name, shellCommand, arrayify!dependenciesFunc, arrayify!implicitsFunc);
189 }
190 
191 
192 //end of rules
193 
194 private auto arrayify(alias func)() {
195     import std.traits;
196     auto ret = func();
197     static if(isArray!(typeof(ret)))
198         return ret;
199     else
200         return [ret];
201 }
202 
203 auto sourcesToTargets(alias sourcesFunc = Sources!())() {
204     return sourcesToFileNames!sourcesFunc.map!(a => Target(a));
205 }
206 
207 string[] sourcesToFileNames(alias sourcesFunc = Sources!())() @trusted {
208     import std.exception: enforce;
209     import std.file;
210     import std.path: buildNormalizedPath, buildPath;
211     import std.array: array;
212     import std.traits: isCallable;
213     import reggae.config: options;
214 
215     auto srcs = sourcesFunc();
216 
217     DirEntry[] modules;
218     foreach(dir; srcs.dirs.value.map!(a => buildPath(options.projectPath, a))) {
219         enforce(isDir(dir), dir ~ " is not a directory name");
220         auto entries = dirEntries(dir, SpanMode.depth);
221         auto normalised = entries.map!(a => DirEntry(buildNormalizedPath(a)));
222 
223         modules ~= normalised.filter!(a => !a.isDir).array;
224     }
225 
226     foreach(module_; srcs.files.value) {
227         modules ~= DirEntry(buildNormalizedPath(buildPath(options.projectPath, module_)));
228     }
229 
230     return modules.
231         map!(a => a.name.removeProjectPath).
232         filter!(srcs.filterFunc).
233         filter!(a => a != "reggaefile.d").
234         array;
235 }
236 
237 //run-time version
238 string[] sourcesToFileNames(in string projectPath,
239                             in string[] srcDirs,
240                             const(string)[] excDirs,
241                             in string[] srcFiles,
242                             in string[] excFiles) @trusted {
243 
244 
245 
246     import std.exception: enforce;
247     import std.file;
248     import std.path: buildNormalizedPath, buildPath;
249     import std.array: array;
250     import std.traits: isCallable;
251 
252     excDirs = (excDirs ~ ".reggae").map!(a => buildPath(projectPath, a)).array;
253 
254     DirEntry[] files;
255     foreach(dir; srcDirs.map!(a => buildPath(projectPath, a))) {
256         enforce(isDir(dir), dir ~ " is not a directory name");
257 
258         auto entries = dirEntries(dir, SpanMode.depth)
259                 .map!(a => a.buildNormalizedPath)
260                 .filter!(a => !excDirs.canFind!(b => a.dirName.absolutePath.startsWith(b)));
261         files ~= entries.map!(a => DirEntry(a)).array;
262     }
263 
264     foreach(module_; srcFiles) {
265         files ~= DirEntry(buildNormalizedPath(buildPath(projectPath, module_)));
266     }
267 
268     return files.
269         map!(a => removeProjectPath(projectPath, a.name)).
270         filter!(a => !excFiles.canFind(a)).
271         filter!(a => a != "reggaefile.d").
272         array;
273 }
274 
275 
276 //run-time version
277 Target[] objectFiles(in string projectPath,
278                      in string[] srcDirs,
279                      in string[] excDirs,
280                      in string[] srcFiles,
281                      in string[] excFiles,
282                      in string flags = "",
283                      in string[] includes = [],
284                      in string[] stringImports = []) @trusted {
285 
286     return srcFilesToObjectTargets(sourcesToFileNames(projectPath, srcDirs, excDirs, srcFiles, excFiles),
287                                    Flags(flags),
288                                    const ImportPaths(includes),
289                                    const StringImportPaths(stringImports));
290 }
291 
292 //run-time version
293 Target[] staticLibrary(in string projectPath,
294                        in string name,
295                        in string[] srcDirs,
296                        in string[] excDirs,
297                        in string[] srcFiles,
298                        in string[] excFiles,
299                        in string flags,
300                        in string[] includes,
301                        in string[] stringImports) @trusted {
302 
303 
304     version(Posix)
305         return staticLibraryTarget(name,
306                                    objectFiles(projectPath, srcDirs, excDirs, srcFiles, excFiles, flags, includes, stringImports));
307     else
308         throw new Exception("Can only create static libraries on Posix");
309 }
310 
311 Target[] staticLibraryTarget(in string name, Target[] objects) {
312     import std.path: extension;
313     const realName = name.extension == libExt ? name : name ~ libExt;
314     return [Target([buildPath("$builddir", realName)],
315                    staticLibraryShellCommand,
316                    objects)];
317 }
318 
319 private enum staticLibraryShellCommand = "ar rcs $out $in";
320 
321 private Target[] srcFilesToObjectTargets(in string[] srcFiles,
322                                          in Flags flags,
323                                          in ImportPaths includes,
324                                          in StringImportPaths stringImports) {
325 
326     const dSrcs = srcFiles.filter!(a => a.getLanguage == Language.D).array;
327     auto otherSrcs = srcFiles.filter!(a => a.getLanguage != Language.D && a.getLanguage != Language.unknown);
328     import reggae.rules.d: dlangObjectFiles;
329     return
330         dlangObjectFiles(dSrcs, flags.value, ["."] ~ includes.value, stringImports.value) ~
331         otherSrcs.map!(a => objectFile(SourceFile(a), flags, includes)).array;
332 }
333 
334 
335 version(Windows) {
336     immutable objExt = ".obj";
337     immutable exeExt = ".exe";
338     immutable libExt = ".lib";
339 } else {
340     immutable objExt = ".o";
341     immutable exeExt = "";
342     immutable libExt = ".a";
343 }
344 
345 package string objFileName(in string srcFileName) @safe pure {
346     import reggae.path: deabsolutePath;
347     import std.path: stripExtension, defaultExtension, isRooted;
348     import std.array: replace;
349 
350     immutable localFileName = srcFileName.deabsolutePath;
351 
352     return localFileName.stripExtension.defaultExtension(objExt).replace("..", "__");
353 }
354 
355 string removeProjectPath(in string path) @safe {
356     import std.path: relativePath, absolutePath;
357     import reggae.config: options;
358     //relativePath is @system
359     return () @trusted { return path.absolutePath.relativePath(options.projectPath.absolutePath); }();
360 }
361 
362 string removeProjectPath(in string projectPath, in string path) @safe pure {
363     import std.path: relativePath, absolutePath;
364     //relativePath is @system
365     return () @trusted { return path.absolutePath.relativePath(projectPath.absolutePath); }();
366 }
367 
368 
369 
370 Command compileCommand(in string srcFileName,
371                        in string flags = "",
372                        in string[] includePaths = [],
373                        in string[] stringImportPaths = [],
374                        in string projDir = "$project",
375                        Flag!"justCompile" justCompile = Yes.justCompile) @safe pure {
376 
377     string maybeExpand(string path) {
378         return path.startsWith(gBuilddir) ? expandBuildDir(path) : buildPath(projDir, path);
379     }
380 
381     auto includeParams = includePaths.map!(a => "-I" ~ maybeExpand(a)). array;
382     auto flagParams = flags.splitter.array;
383     immutable language = getLanguage(srcFileName);
384 
385     auto params = [assocEntry("includes", includeParams),
386                    assocEntry("flags", flagParams)];
387 
388     if(language == Language.D)
389         params ~= assocEntry("stringImports",
390                              stringImportPaths.map!(a => "-J" ~ maybeExpand(a)).array);
391 
392     params ~= assocEntry("DEPFILE", [srcFileName.objFileName ~ ".dep"]);
393 
394     immutable type = justCompile ? CommandType.compile : CommandType.compileAndLink;
395     return Command(type, assocList(params));
396 }
397 
398 
399 enum Language {
400     C,
401     Cplusplus,
402     D,
403     unknown,
404 }
405 
406 Language getLanguage(in string srcFileName) @safe pure nothrow {
407     switch(srcFileName.extension) with(Language) {
408     case ".d":
409         return D;
410     case ".cpp":
411     case ".CPP":
412     case ".C":
413     case ".cxx":
414     case ".c++":
415     case ".cc":
416         return Cplusplus;
417     case ".c":
418         return C;
419     default:
420         return unknown;
421     }
422 }