rbuildsys 0.0.1.pre → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 907496c16fb0c9c5b7ee4fcc5a177cdf4d25f58603c952e012b2d9304355fb0d
4
- data.tar.gz: 20044f4d5b9ff2627780e2c43c3d623adf1137e3171ce9945cdee35d787c6c4e
3
+ metadata.gz: 790cbd7c15d0ba978a5e4ea3503e0fed4075f814ef402f117f05133c700e9d24
4
+ data.tar.gz: adebe238290e2db4786dbe40128c3c870af49b15def92b3c6d7a2002963de225
5
5
  SHA512:
6
- metadata.gz: 81c674e67e81835bcae6a17bbd632c31f25b99aa81b5d4f5421b5fc655ddf269976ecf6944309dd36306133f8b26810aae2222dece06d683807bc3cdcccb0eeb
7
- data.tar.gz: 6b882e5244940eff79357dcff6df5bc10b8d9c96a3fb8e5e18432f78feb4aa0d4ed9a47058fd853ea8072f30cecc20ec0f761c018a25a59a56fbf46455f3959b
6
+ metadata.gz: 7729ecf4bcc8682643226e4ad5c9d2847c66595d776d97f30c04deed260b63460a6e2a041509d05d0b2194dbc0a27e9c8004e3b9c5b673b59f3fa822cb2dfd91
7
+ data.tar.gz: b1a64205974c6c1f61b3a96f9f0833a1bcd5bdb1ee1e5a1e6e75048b9e154997c632a1438b1d2d5894970d39d80254c2f1edb5b7d729b3ab2afdf713ecdc8a1d
Binary file
Binary file
@@ -1,5 +1,6 @@
1
1
  require "optparse"
2
2
  require "fileutils"
3
+ require "json"
3
4
 
4
5
  # Namespace for all code for RBuildSys
5
6
  module RBuildSys
@@ -12,14 +13,31 @@ module RBuildSys
12
13
  # @return [String]
13
14
  attr_reader :name
14
15
 
16
+ # Returns the output name of the project; typically the same as {#name}
17
+ # @return [String]
18
+ attr_accessor :outputName
19
+
20
+ # Returns the base directory for this project
21
+ # @return [String]
22
+ attr_accessor :baseDir
23
+
15
24
  # Returns the array of source directorys for this project
16
25
  # @return [Array<String>]
17
26
  attr_reader :src_dirs
18
27
 
28
+ # Returns the array of source globs for this project
29
+ # @return [Array<String>]
30
+ attr_reader :src_globs
31
+
19
32
  # Returns the array of include directorys for this project
20
33
  # @return [Array<String>]
21
34
  attr_reader :inc_dirs
22
35
 
36
+ # Returns the array of "public" include directorys for this project.
37
+ # These will only be used by projects that depends on this project
38
+ # @return [Array<String>]
39
+ attr_reader :public_inc_dirs
40
+
23
41
  # Returns the array of library directorys for this project
24
42
  # @return [Array<String>]
25
43
  attr_reader :lib_dirs
@@ -36,36 +54,70 @@ module RBuildSys
36
54
  # @return [true, false]
37
55
  attr_accessor :no_install
38
56
 
39
- # Returns the library type of this project; if nil, this project isn't a library
40
- # @return [:static, :dynamic, :s, :dyn]
57
+ # Returns the library type of this project; if nil, this project isn't a library.
58
+ # +:both+ is when the project can be static AND dynamic without any changes inside the sourcecode.
59
+ # @return [:static, :dynamic, :s, :dyn, :both]
41
60
  attr_accessor :libType
42
61
 
43
62
  # Returns the dependencys of this project
44
63
  # @return [Array<Project>]
45
64
  attr_reader :dependencys
46
65
 
47
- # @param name [String] name of the project; see {#name}
48
- # @param options [Hash] various options for the project
49
- # @option options :lang [Symbol] language used for this project. Available: +:c+, +:cpp+
50
- # @option options :srcFile_endings [Array<String>] additional fileendings that should be used for finding sourcefiles
51
- # @option options :no_install [true, false] flag that tells if the project is install-able or not; see {#no_install}
52
- # @option options :libType [:static, :dynamic, :s, :dyn, nil] type of library; see {#libType}
53
- # @option options :compiler [String, nil] compiler to be used to transform sourcefiles to objectfiles; if no is supplied, +"gcc"+ or +"g++"+ is used (based on the language)
54
- def initialize(name, options)
66
+ # Returns the toolchain used for this project
67
+ # @return [Hash]
68
+ attr_reader :toolchain
69
+
70
+ # Returns the defined symbols and its value for this project
71
+ # @return [Hash]
72
+ attr_reader :defines
73
+
74
+ # Returns the defined symbols and its value for config files for this project
75
+ # @return [Hash]
76
+ attr_reader :config_symbols
77
+
78
+ # Returns the config files that should be configured
79
+ # @return [Array<Hash>]
80
+ attr_reader :config_files
81
+
82
+ # Initializes a new instance of this class.
83
+ # Default language is "c".
84
+ #
85
+ # @param name [String] Name of the project; see {#name}
86
+ # @param options [Hash] Various options for the project
87
+ # @option options :lang [Symbol] Language used for this project. Available: +:c+, +:cpp+
88
+ # @option options :toolchain [String] Toolchain that should be used; if no is supplied, "gnu" is used.
89
+ # @option options :srcFile_endings [Array<String>] Additional fileendings that should be used for finding sourcefiles
90
+ # @option options :no_install [true, false] Flag that tells if the project is install-able or not; see {#no_install}
91
+ # @option options :libType [:static, :dynamic, :s, :dyn, :both, nil] Type of library; see {#libType}
92
+ # @option options :outputName [String] If the output name shoud differ from the project name, specify it here
93
+ # @option options :baseDir [String] Directory that should be the base of the project; doesn't change the build directory
94
+ def initialize(name, options = {})
95
+ if (!options.is_a?(Hash)) then
96
+ raise ArgumentError.new("Argument #2 (options) need to be an hash!");
97
+ end
98
+
55
99
  @name = name;
56
100
  @src_dirs = [];
101
+ @src_globs = [];
57
102
  @inc_dirs = [];
103
+ @public_inc_dirs = [];
58
104
  @lib_dirs = [];
59
105
  @dependencys = [];
60
106
  @librarys = [];
61
107
  @flags = [];
108
+ @defines = {};
109
+ @config_symbols = {};
110
+ @config_files = [];
62
111
 
63
112
  check_lang(options[:lang] || "c");
64
-
65
- @compiler = "gcc" if (@lang == :c);
66
- @compiler = "g++" if (@lang == :cpp);
67
- if (options[:compiler]) then
68
- @compiler = options[:compiler];
113
+ if (OPTIONS[:toolchainOverride]) then
114
+ if (!load_and_check_toolchain(OPTIONS[:toolchainOverride])) then
115
+ raise RuntimeError.new("Commandline specified a toolchain override, but toolchain cannot be found: '#{@toolchain_name}'");
116
+ end
117
+ else
118
+ if (!load_and_check_toolchain(options[:toolchain] || "gnu")) then
119
+ raise ArgumentError.new("Argument #2 (options) contains key toolchain, but toolchain cannot be found: '#{@toolchain_name}'");
120
+ end
69
121
  end
70
122
 
71
123
  @src_file_endings = [];
@@ -79,6 +131,32 @@ module RBuildSys
79
131
 
80
132
  @no_install = options[:no_install] || false;
81
133
  @libType = options[:libType];
134
+ @outputName = options[:outputName] || @name;
135
+ @baseDir = options[:baseDir] || ".";
136
+ end
137
+
138
+ private
139
+ def load_and_check_toolchain(name)
140
+ @toolchain_name = name;
141
+ @toolchain = TOOLCHAINS[@toolchain_name];
142
+ if (!@toolchain) then
143
+ return false;
144
+ end
145
+
146
+ @compiler = @toolchain["compiler"]["c"] if (@lang == :c);
147
+ @compiler = @toolchain["compiler"]["c++"] if (@lang == :cpp);
148
+ check_binary(@compiler);
149
+
150
+ @archiver = @toolchain["archiver"];
151
+ check_binary(@archiver);
152
+
153
+ return true;
154
+ end
155
+
156
+ private
157
+ def check_binary(bin)
158
+ `which #{bin}`;
159
+ raise RuntimeError.new("Trying to use binary '#{bin}', but binary dosnt exists or is not in PATH") if ($?.exitstatus != 0);
82
160
  end
83
161
 
84
162
  private
@@ -86,6 +164,11 @@ module RBuildSys
86
164
  # https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html
87
165
  # or simply the language: c, c++, gnu, gnu++
88
166
  def check_lang(lang)
167
+ if ([:c, :cpp].include?(lang)) then
168
+ @lang = lang;
169
+ return;
170
+ end
171
+
89
172
  if ((m = lang.match(/^(c\+\+|gnu\+\+)([\dxyza]+)?$/)) != nil) then
90
173
  @lang = :cpp;
91
174
  @c_standard = lang if (m[2]);
@@ -104,22 +187,133 @@ module RBuildSys
104
187
  return;
105
188
  end
106
189
 
107
- raise ArgumentError.new("argument #2 contains key lang, but cannot validate it: '#{lang}'");
190
+ raise ArgumentError.new("Initializer argument #2 (options) contains key lang, but cannot validate it: '#{lang}'");
108
191
  end
109
192
 
110
193
  public
111
- # cleans the project's output directory
194
+ # Cleans the project's output directory
112
195
  def clean()
113
196
  buildDir = "./build/#{name}";
114
197
  FileUtils.remove_dir(buildDir) if File.directory?(buildDir)
115
198
  end
116
199
 
200
+ private
201
+ def hasSymbol(sym)
202
+ return true if (@config_symbols.keys.include?(sym));
203
+ return CONFIG_SYMBOLS.keys.include?(sym);
204
+ end
205
+
206
+ private
207
+ def getSymbolValue(sym)
208
+ return @config_symbols[sym] if (@config_symbols.keys.include?(sym));
209
+ return CONFIG_SYMBOLS[sym];
210
+ end
211
+
212
+ public
213
+ # Configure a specific file
214
+ #
215
+ # @param input [String] The filename of the file that should be configured
216
+ # @param output [String] The filename that should be used to save the result, cannot be the same as the input!
217
+ # @param options [Hash] Some optional options
218
+ # @option options :realUndef [Boolean] If this is true, not defined symbols will be undefined with '#undef <symbol>'
219
+ def configure_file(input, output, options = {})
220
+ # based on https://cmake.org/cmake/help/latest/command/configure_file.html
221
+
222
+ puts("- configure: #{input}");
223
+
224
+ if (input.is_a?(String)) then
225
+ data = File.read(input);
226
+ end
227
+
228
+ # replace cmake defines
229
+ cmake_defines = data.to_enum(:scan, /\#cmakedefine ([a-zA-Z0-9_]+)([^\n]+)?/).map { Regexp.last_match };
230
+ for cmake_def in cmake_defines do
231
+ if (hasSymbol(cmake_def[1])) then
232
+ data.sub!(cmake_def[0], "#define #{cmake_def[1]}");
233
+ else
234
+ if (options[:realUndef]) then
235
+ data.sub!(cmake_def[0], "#undef #{cmake_def[1]}");
236
+ else
237
+ data.sub!(cmake_def[0], "/* #undef #{cmake_def[1]} */");
238
+ end
239
+ end
240
+ end
241
+
242
+ # replace variables!
243
+ matches = data.to_enum(:scan, /\@([a-zA-Z0-9_]+)\@/).map { Regexp.last_match };
244
+ for match in matches do
245
+ if (hasSymbol(match[1])) then
246
+ data.sub!(match[0], getSymbolValue(match[1]).inspect);
247
+ else
248
+ puts "[WARN] in file #{input}: #{match[0]} found, but no value for it defined!";
249
+ end
250
+ end
251
+
252
+ File.write(output, data);
253
+ end
254
+
255
+ public
256
+ # Tests if the toolchain for this project is for windows.
257
+ #
258
+ # @return [Boolean] Returns true if windows, false otherwise
259
+ def isWindows?()
260
+ return @toolchain["os"] == "windows";
261
+ end
262
+
263
+ public
264
+ # Tests if the toolchain for this project is for macos.
265
+ #
266
+ # @return [Boolean] Returns true if macos, false otherwise
267
+ def isMac?()
268
+ return @toolchain["os"] == "macos";
269
+ end
270
+
271
+ public
272
+ # Tests if the toolchain for this project is for linux.
273
+ #
274
+ # @return [Boolean] Returns true if linux, false otherwise
275
+ def isLinux?()
276
+ return @toolchain["os"] == "linux";
277
+ end
278
+
279
+ private
280
+ def checkDependencys()
281
+ @dependencys.each_index {|idx|
282
+ dep = @dependencys[idx];
283
+ if (dep.is_a?(Array)) then
284
+ name = dep[0];
285
+
286
+ # use installed project
287
+ proj_conf_path = File.join(getInstallPath(), "lib", "rbuildsys_conf", "#{name}.config.json");
288
+ if (!File.exists?(proj_conf_path)) then
289
+ raise RuntimeError.new("Could not find project '#{name}'!");
290
+ end
291
+
292
+ proj_conf = JSON.parse(File.read(proj_conf_path));
293
+ if (!["static", "dynamic", "both"].include?(proj_conf["libType"])) then
294
+ raise RuntimeError.new("'#{name}' can't be used as a dependency because it is not a library");
295
+ end
296
+ if (proj_conf["libType"] != dep[1].to_s || proj_conf["libType"] == "both") then
297
+ raise RuntimeError.new("'#{name}' can't be linked with linktype #{dep[1]}");
298
+ end
299
+
300
+ if (proj_conf["toolchain"] != @toolchain_name) then
301
+ raise RuntimeError.new("Dependency '#{name}' was compiled using the toolchain #{proj_conf["toolchain"]}, while this project trys to use #{@toolchain_name}!");
302
+ end
303
+
304
+ @dependencys[idx] = proj_conf;
305
+ end
306
+ }
307
+ end
308
+
117
309
  public
118
- # builds the project
119
- # @return [true, false] state of the build, true means success, false means failure
310
+ # Builds the project
311
+ # @return [true, false] State of the build, true means success, false means failure
120
312
  def build()
121
- for dep in dependencys do
122
- if (!dep.build()) then
313
+ for dep in @dependencys do
314
+ next if (OPTIONS[:cleanBuild]);
315
+
316
+ if (dep.is_a?(Project) && !dep.build()) then
123
317
  return false;
124
318
  end
125
319
  end
@@ -127,28 +321,57 @@ module RBuildSys
127
321
  puts "Now building '#{@name}'";
128
322
  puts "- using language #{@lang}";
129
323
 
130
- for dep in dependencys do
131
- @inc_dirs.push(*dep.inc_dirs).uniq!;
132
- @lib_dirs.push("./build/#{dep.name}");
133
- @librarys.push(dep.name);
134
- if (dep.libType == :static) then
135
- @lib_dirs.push(*dep.lib_dirs).uniq!
136
- @librarys.push(*dep.librarys).uniq!
324
+ checkDependencys();
325
+ #pp @dependencys.map{|dep| if (dep.is_a?(Project)) then "local #{dep.name}" else "installed #{dep["name"]}" end }
326
+
327
+ # TODO: we dont check if previous build files where compiled with the current toolchain or not!
328
+
329
+ for config_file in @config_files do
330
+ configure_file(config_file[:input], config_file[:output], config_file[:options]);
331
+ end
332
+
333
+ for dep in @dependencys do
334
+ if (dep.is_a?(Project)) then
335
+ @inc_dirs.push(*dep.public_inc_dirs).uniq!;
336
+ @lib_dirs.push("./build/#{dep.name}");
337
+ @librarys.push(dep.name);
338
+ if (dep.libType == :static) then
339
+ @lib_dirs.push(*dep.lib_dirs).uniq!
340
+ @librarys.push(*dep.librarys).uniq!
341
+ end
342
+ elsif (dep.is_a?(Hash)) then
343
+ # dependency is an globaly installed library
344
+ @inc_dirs.push(File.join(getInstallPath(), "include", dep["name"])).uniq!;
345
+ @lib_dirs.push(File.join(getInstallPath(), "lib")).uniq!;
346
+ @librarys.push(dep["name"]);
347
+ if (dep["libType"] == "static") then
348
+ @lib_dirs.push(*dep["lib_dirs"]).uniq!;
349
+ @librarys.push(*dep["librarys"]).uniq!;
350
+ end
137
351
  end
138
352
  end
139
353
 
140
- @flags.push("-g") if (OPTIONS[:debug]);
354
+ @flags.push(@toolchain["flags"]["debug"]) if (OPTIONS[:debug]);
355
+ @flags.push(@toolchain["extra_flags"]) if (!@toolchain["extra_flags"].strip.empty?);
141
356
 
142
357
  # create the project build dir
143
- buildDir = "./build/#{name}";
144
- system("mkdir -p #{buildDir}"); # todo: dont use system!
358
+ buildDir = "./build/#{@name}";
359
+ FileUtils.mkdir_p(buildDir);
145
360
 
146
361
  # make the includes
147
362
  #inc_dirs.map! { |incDir| incDir + "/*.h" }
148
363
  #includes = Dir.glob(inc_dirs);
149
- includes = @inc_dirs.map{|incDir| "-I #{incDir}"}.join(" ");
150
- libs = @lib_dirs.map{|libDir| "-L #{libDir}"}.join(" ") + " " + @librarys.map{|lib| "-l #{lib}"}.join(" ");
151
- std = @c_standard ? ("-std=#{@c_standard}") : "";
364
+ includes = @inc_dirs.map{|incDir| "#{@toolchain["flags"]["include"]} #{incDir}"}.join(" ");
365
+ libs = @lib_dirs.map{|libDir| "#{@toolchain["flags"]["libPath"]} #{libDir}"}.join(" ") + " " + @librarys.map{|lib| "#{@toolchain["flags"]["libLink"]} #{lib}"}.join(" ");
366
+ std = @c_standard ? ("#{@toolchain["flags"]["langStd"]}=#{@c_standard}") : "";
367
+
368
+ defines = @defines.map{|sym,val|
369
+ if val == nil then
370
+ "#{@toolchain["flags"]["define"]}#{sym}"
371
+ else
372
+ "#{@toolchain["flags"]["define"]}#{sym}=#{val}"
373
+ end
374
+ }.join(" ");
152
375
 
153
376
  # for now, just ignore all global and relative paths for sources
154
377
  src_dirs.select! { |srcDir|
@@ -156,71 +379,202 @@ module RBuildSys
156
379
  }
157
380
 
158
381
  source_modified = false;
159
- build_failed = false;
382
+ build_failed = false;
383
+
384
+ build_file = ->(srcFile, srcDir) {
385
+ #objFile = File.join(buildDir, srcFile.gsub(srcDir, ""));
386
+ objFile = File.join(buildDir, srcFile);
387
+ objFile = objFile.gsub(Regexp.new("\.(#{@src_file_endings.join("|")})$"), ".o");
388
+ srcTime = File.mtime(srcFile);
389
+
390
+ if (OPTIONS[:cleanBuild] || OPTIONS[:cleanBuildAll] || !File.exists?(objFile) || (srcTime > File.mtime(objFile))) then
391
+ source_modified = true;
392
+ FileUtils.mkdir_p(File.dirname(objFile)); # ensure we have the parent dir(s)
393
+
394
+ # build the source!
395
+ cmd = "#{@compiler}"
396
+ cmd += " #{std}" if (!std.empty?);
397
+ cmd += " #{@toolchain["flags"]["pic"]}" if (@libType == :dynamic || @libType == :both);
398
+ cmd += " #{includes}";
399
+ cmd += " #{defines}" if (defines.size > 0);
400
+ cmd += " #{@flags.join(" ")}" if (@flags.size > 0);
401
+ cmd += " -c #{@toolchain["flags"]["output"]} #{objFile} #{srcFile}";
402
+ puts "- $ #{cmd}";
403
+ f = system(cmd);
404
+ if (f) then
405
+ FileUtils.touch(objFile, :mtime => srcTime);
406
+ else
407
+ build_failed = true;
408
+ end
409
+ end
410
+ }
160
411
 
161
412
  src_dirs.each { |srcDir|
162
413
  globStr = File.join(srcDir, "**/*.{#{@src_file_endings.join(",")}}");
163
414
  srcFiles = Dir.glob(globStr);
415
+ srcFiles.each { |srcFile|
416
+ build_file.call(srcFile, srcDir)
417
+ }
418
+ }
164
419
 
165
- # do the incremental build!
420
+ src_globs.each{ |glob|
421
+ srcFiles = Dir.glob(glob);
166
422
  srcFiles.each { |srcFile|
167
- objFile = File.join(buildDir, srcFile.gsub(srcDir, ""));
168
- objFile = objFile.gsub(Regexp.new("\.(#{@src_file_endings.join("|")})$"), ".o");
169
- srcTime = File.mtime(srcFile);
170
-
171
- if (OPTIONS[:cleanBuild] || !File.exists?(objFile) || (srcTime > File.mtime(objFile))) then
172
- source_modified = true;
173
- FileUtils.mkdir_p(File.dirname(objFile)); # ensure we have the parent dir(s)
174
-
175
- # build the source!
176
- cmd = "#{@compiler} #{std} -Wall #{includes} #{@flags.join(" ")} -c -o #{objFile} #{srcFile}";
177
- puts "- $ #{cmd}";
178
- f = system(cmd);
179
- if (f) then
180
- FileUtils.touch(objFile, :mtime => srcTime);
181
- else
182
- build_failed = true;
183
- end
184
- end
423
+ build_file.call(srcFile, File.dirname(srcFile))
185
424
  }
186
425
  }
187
426
 
188
427
  if (build_failed) then
428
+ puts "Build failed, see log for details!";
189
429
  return false;
190
430
  end
191
431
 
192
- if (!source_modified) then
193
- puts "- No need for building library/executable, nothing changed!";
194
- return true;
195
- end
196
-
197
432
  objFiles = Dir.glob(buildDir + "/**/*.o");
198
- if (libType == :static) then
199
- puts "- Building static library lib#{name}!";
200
- libname = File.join(buildDir, "lib#{name}.a");
201
- cmd = "ar rcs #{libname} #{objFiles.join(" ")}";
202
- puts " - $ #{cmd}";
203
- f = system(cmd);
433
+ if (@libType != nil) then
434
+ f = true;
435
+
436
+ if (@libType == :static || @libType == :both) then
437
+
438
+ libname = @toolchain["output_filenames"]["staticLib"].clone;
439
+ libname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName);
440
+ libpath = File.join(buildDir, libname);
441
+
442
+ if (!File.exists?(libpath) || source_modified) then
443
+ puts "- Building static library #{libname}!";
444
+ cmd = "#{@archiver} rcs #{libpath} #{objFiles.join(" ")}";
445
+ puts " - $ #{cmd}";
446
+ f_static = system(cmd);
447
+ f = false if (!f_static);
448
+ else
449
+ puts "- No need for building library, nothing changed!";
450
+ end
451
+ end
452
+
453
+ if (@libType == :dynamic || @libType == :both) then
454
+ #libname = @toolchain["output_filenames"]["dynamicLib"].clone;
455
+ #libname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName);
456
+ #puts "- Building dynamic library #{libname}!";
457
+ #libname = File.join(buildDir, "lib#{name}.so");
458
+ #cmd = ""
459
+ puts "[WARN] dynamic librarys are not implemented yet!";
460
+ end
461
+
462
+ metadata = {
463
+ name: @outputName,
464
+ libType: @libType,
465
+ lib_dirs: @lib_dirs,
466
+ librarys: @librarys,
467
+ dependencys: @dependencys.map {|dep|
468
+ if (dep.is_a?(Project)) then
469
+ dep.outputName
470
+ else
471
+ dep["name"];
472
+ end
473
+ },
474
+ toolchain: @toolchain_name
475
+ };
476
+ # TODO: copy the toolchain definition, if the toolchain is not permanently installed on the system!
477
+ metadataFile = File.join(buildDir, "#{@outputName}.config.json");
478
+ File.write(metadataFile, JSON.pretty_generate(metadata));
479
+
204
480
  return f;
205
- elsif (libType == :dynamic) then
206
- raise NotImplementedError.new("");
207
481
  else
208
482
  # build runable binary
209
- binname = File.join(buildDir, "#{name}.run");
210
- puts "- Building executable #{binname}!";
211
- cmd = "#{@compiler} #{includes} #{@flags.join(" ")} -o #{binname} #{objFiles.join(" ")} #{libs}";
212
- puts " - $ #{cmd}";
213
- f = system(cmd);
214
- return f;
483
+ binname = @toolchain["output_filenames"]["exec"].clone;
484
+ binname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName);
485
+ binpath = File.join(buildDir, binname);
486
+
487
+ if (!File.exists?(binpath) || source_modified) then
488
+ puts "- Building executable #{binname}!";
489
+ cmd = "#{@compiler} #{includes}"
490
+ cmd += " #{@flags.join(" ")}" if (@flags.size > 0)
491
+ cmd += " #{@toolchain["flags"]["output"]} #{binpath} #{objFiles.join(" ")} #{libs}";
492
+ puts " - $ #{cmd}";
493
+ f = system(cmd);
494
+ return f;
495
+ else
496
+ puts "- No need for building binary, nothing changed!";
497
+ end
215
498
  end
216
499
 
217
500
  return true;
218
501
  end
502
+
503
+ public
504
+ # Installs the project
505
+ def install()
506
+ dir = (File.expand_path(OPTIONS[:installDir]) || "/usr/local") if (isLinux? || isMac?)
507
+ dir = (File.expand_path(OPTIONS[:installDir]) || "C:/Program Files/#{@outputName}") if (isWindows?)
508
+
509
+ puts "RBuildSys will install #{@name} to the following location: #{dir}";
510
+ puts "Do you want to proceed? [y/N]: "
511
+ if ( STDIN.readline.strip != "y" ) then
512
+ puts "Aborting installation...";
513
+ exit(1);
514
+ end
515
+
516
+ # 1. install all includes
517
+ incDir = File.join(dir, "include", @outputName);
518
+ if (!Dir.exists?(incDir)) then
519
+ puts "- create dir: #{incDir}";
520
+ FileUtils.mkdir_p(incDir);
521
+ end
522
+
523
+ @public_inc_dirs.each {|d|
524
+ files = Dir.glob(File.join("#{d}", "**/*.{h,hpp}"));
525
+ files.each {|f|
526
+ dest = File.join(incDir, f.gsub(d, ""));
527
+ puts "- install: #{dest}";
528
+ destDir = File.dirname(dest);
529
+ if (!Dir.exists?(destDir)) then
530
+ puts "- create dir: #{destDir}";
531
+ FileUtils.mkdir_p(destDir);
532
+ end
533
+ FileUtils.copy_file(f, dest)
534
+ }
535
+ }
536
+
537
+ # 2.1 install library results (if any)
538
+ buildDir = "./build/#{@name}";
539
+ if (@libType) then
540
+ libDir = File.join(dir, "lib");
541
+ if (!Dir.exists?(libDir)) then
542
+ puts "- create dir: #{libDir}";
543
+ FileUtils.mkdir_p(libDir);
544
+ end
545
+
546
+ files = Dir.glob(File.join(buildDir, "*.{a,lib,so,dll}")); # TODO: this glob should based on the toolchain definition
547
+ files.each {|f|
548
+ dest = File.join(libDir, f.gsub(buildDir, ""));
549
+ puts "- install: #{dest}";
550
+ FileUtils.copy_file(f, dest)
551
+ }
552
+
553
+ # install definitions so we can use them in other projects easier!
554
+ metadataDir = File.join(libDir, "rbuildsys_conf");
555
+ if (!Dir.exists?(metadataDir)) then
556
+ puts "- create dir: #{metadataDir}";
557
+ FileUtils.mkdir_p(metadataDir);
558
+ end
559
+ metadataFile = File.join(metadataDir, "#{@outputName}.config.json");
560
+ puts "- install: #{metadataFile}";
561
+ FileUtils.copy_file(File.join(buildDir, "#{@outputName}.config.json"), metadataFile);
562
+ end
563
+
564
+ # 2.2 install executable results
565
+ if (!@libType) then
566
+ raise NotImplementedError.new("installation of executables (*.exe, *.run etc.) is not supported yet");
567
+ end
568
+
569
+ end
219
570
  end
220
571
 
221
572
  # Stores all projects that are defined by the user
222
573
  PROJECTS = {};
223
574
 
575
+ private
576
+ DECLARED_PROJECTS = [];
577
+
224
578
  # Holds the current project that is currently being configured.
225
579
  # Only non-nil inside the block for {#newProject}
226
580
  @@cur_proj = nil;
@@ -234,177 +588,511 @@ module RBuildSys
234
588
  # true if we only want to clean our build directory
235
589
  :clean => false,
236
590
 
237
- # true if we want to clean all buildfiles before building
591
+ # true if we want to make a clean rebuild for only the specified project(s)
238
592
  :cleanBuild => false,
239
593
 
594
+ # true if we want to make a clean rebuild for the specified project(s) and its dependencys
595
+ :cleanBuildAll => false,
596
+
240
597
  # true if we want to install instead of building
241
598
  :install => false,
599
+
600
+ # contains the path where to install the project(s)
601
+ :installDir => ENV["RBUILDSYS_INSTALLDIR"],
602
+
603
+ # overrides the default toolchain specified in the build-script
604
+ :toolchainOverride => nil,
242
605
  };
243
606
 
607
+ # Stores all currently loaded toolchains
608
+ TOOLCHAINS = {};
609
+
610
+ # Stores all global symbols to configure files
611
+ CONFIG_SYMBOLS = {};
612
+
244
613
  # Option parser of RBuildSys, used to provide standard operations such as clean, clean build, install...
245
614
  @@optparse = OptionParser.new do |opts|
246
- opts.banner = "Usage: #{$PROGRAM_NAME} [options] <projects>";
615
+
616
+ class MyBanner
617
+ def to_s
618
+ gemDir = File.dirname(File.expand_path(__FILE__));
619
+ toolchains = Dir.glob("#{gemDir}/toolchains/*.json");
620
+ toolchains.map!{|f| File.basename(f, '.json') }
621
+
622
+ return "Usage: #{$PROGRAM_NAME} [options] <projects>\n" +
623
+ "Projects:\n" +
624
+ " " + (PROJECTS.keys + DECLARED_PROJECTS).join(", ") + "\n" +
625
+ "Toolchains:\n" +
626
+ " builtin: " + toolchains.join(", ") + "\n" +
627
+ " user : " + (TOOLCHAINS.keys - toolchains).join(", ") + "\n" +
628
+ "Options:";
629
+ end
630
+ end
631
+ opts.banner = MyBanner.new();
247
632
 
248
633
  opts.on("-r","--release", "Build for release") do
249
634
  # todo: if compiled for debug before, the system cannot detect the change
250
635
  OPTIONS[:debug] = false;
251
636
  end
252
- opts.on("-b", "--build-clean", "Make a clean build") do
637
+ opts.on("--rebuild", "Make a clean rebuild only for the given project(s)") do
253
638
  OPTIONS[:cleanBuild] = true;
254
639
  end
255
- opts.on("-c", "--clean", "Clean all projects build dirs") do
640
+ opts.on("--rebuild-all", "Make a clean rebuild for the given project(s) and its dependencys") do
641
+ OPTIONS[:cleanBuildAll] = true;
642
+ end
643
+ opts.on("-c", "--clean", "Clean the build dirs for the given project(s)") do
256
644
  OPTIONS[:clean] = true;
257
645
  end
258
- opts.on("-i", "--install", "Install all project(s) that are installable") do
646
+ opts.on("-i", "--install[=DIR]", "Install all project(s) that are installable") do |dir|
647
+ if (dir) then
648
+ OPTIONS[:installDir] = dir;
649
+ end
259
650
  OPTIONS[:install] = true;
260
651
  end
652
+ opts.on("--installDir=DIR", "Sets the directory to use for installation, and search for installed projects. Can also be configured with the environment variable $RBUILDSYS_INSTALLDIR.") do |dir|
653
+ OPTIONS[:installDir] = dir;
654
+ end
655
+ opts.on("-t TOOLCHAIN", "--toolchain=TOOLCHAIN", "Use the specifed toolchain to build the project and its dependencys") do |toolchain|
656
+ if (toolchain =~ /^[a-zA-Z0-9\_\-]+$/) then
657
+ OPTIONS[:toolchainOverride] = toolchain;
658
+ else
659
+ if (File.exists?(toolchain)) then
660
+ OPTIONS[:toolchainOverride] = RBuildSys.loadToolchain(toolchain)["name"];
661
+ else
662
+ raise RuntimeError.new("Commandline argument for toolchain must either be a alpha-numeric-underscore name or an filename!");
663
+ end
664
+ end
665
+ end
666
+ opts.on("-sKEY=VALUE", "Sets the symbol KEY to VALUE for config files") do |str|
667
+ # TODO: this might be not enough to override options already set in projects
668
+ data = str.split("=");
669
+ CONFIG_SYMBOLS[data[0]] = data[1];
670
+ end
261
671
  end
262
672
 
263
- # creates a new project to build
264
- # takes a block to execute, that defines the project. it is immediately executed,
265
- # and has aaccess to the current project (that is created with this method) in {RBuildSys::@@cur_proj RBuildSys::@@cur_proj}.
673
+ # Creates a new project to build.
674
+ # Takes a block to execute, that defines the project. The block is immediately executed,
675
+ # and has access to the current project (that is created with this method) in {RBuildSys::@@cur_proj RBuildSys::@@cur_proj}.
266
676
  #
267
677
  # @yield Block to configure the project
268
678
  #
269
- # @param name [String] name of the project, also used for the final output of the project
270
- # @param options [Hash] various options; for details see {Project#initialize}
679
+ # @param name [String] Name of the project, also used for the final output of the project
680
+ # @param options [Hash] Various options; for details see {Project#initialize}
271
681
  def newProject(name, options = {})
272
- raise ArgumentError.new("name need to be a string") if (name.class != String);
682
+ raise ArgumentError.new("Argument #1 (name) need to be a string") if (name.class != String);
683
+ raise ArgumentError.new("Argument #2 (options) need to be an hash") if (!options.is_a?(Hash));
684
+
273
685
  @@cur_proj = Project.new(name, options);
274
686
  yield
275
687
  PROJECTS[name] = @@cur_proj;
276
688
  @@cur_proj = nil;
277
689
  end
278
690
 
279
- # adds include directorys to use for the current project
691
+ # Begins the build process.
692
+ # Should be called after you configured all your projects with {#newProject}.
693
+ def build()
694
+ parseARGV();
695
+ ARGV.each { |projname|
696
+ proj = PROJECTS[projname];
697
+ if (proj) then
698
+ if (OPTIONS[:clean]) then
699
+ proj.clean();
700
+ elsif (OPTIONS[:install]) then
701
+ proj.install();
702
+ else
703
+ proj.build();
704
+ end
705
+ end
706
+ }
707
+ end
708
+
709
+ # Parses the commandline arguments
710
+ # @note this should be called before any call to {#newProject} and/or {#build}!
711
+ def parseARGV()
712
+ @@optparse.parse!
713
+ if (ARGV.length == 0) then
714
+ puts @@optparse;
715
+ exit(1);
716
+ end
717
+ end
718
+
719
+ # Declares the project's name so they can be displayed in the help text!
720
+ def declareProjects(*projects)
721
+ DECLARED_PROJECTS.push(*projects);
722
+ end
723
+
724
+ # Returns the version of RBuildSys
725
+ # @return ["1.0.0"]
726
+ def sysVersion()
727
+ return "1.0.0";
728
+ end
729
+
730
+ # Checks if this RBuildSys is at minimum the required version.
731
+ # This is only for scripts that dont use systems like bundler etc.
732
+ # If you use bundler or similar dependency management software, you propably should use their features instead of this.
733
+ # @return [Boolean]
734
+ def checkSysVersion(min_version)
735
+ data_min = min_version.split(".");
736
+ data_ver = sysVersion().split(".");
737
+
738
+ if (data_ver.size != data_min.size) then
739
+ return false;
740
+ end
741
+ if (data_ver.size == 4 && data_ver[3] != data_min[3]) then
742
+ # 4th part dosnt match. Should be something like 'pre' or similar.
743
+ return false;
744
+ end
745
+
746
+ data_min.pop();
747
+ data_ver.pop();
748
+
749
+ while (data_ver.size > 0) do
750
+ a = data_min.pop();
751
+ b = data_ver.pop();
752
+ return false if (a > b);
753
+ end
754
+
755
+ return true;
756
+ end
757
+
758
+ #########################################################################################
759
+ # Project configuration #
760
+ #########################################################################################
761
+
762
+ # Adds include directorys to use for the current project
280
763
  #
281
- # @note should only be called inside the block for {#newProject}
282
- # @param dir [String, Array<String>] directory path; root of the include path. if its a Array, each element is used in a call to {#incDir}
283
- # @param more [Array<String>] additional include paths; each element is used in a call to {#incDir}
764
+ # @note Should only be called inside the block for {#newProject}
765
+ # @param dir [String, Array<String>] Directory path; root of the include path. If its a Array, each element is used in a call to {#incDir}
766
+ # @param more [Array<String>] Additional include paths; each element is used in a call to {#incDir}
284
767
  def incDir(dir, *more)
285
768
  if (dir.class == String) then
286
- raise ArgumentError.new("argument #1 is no directory: '#{dir}'") if (!Dir.exists?(dir));
287
- @@cur_proj.inc_dirs.push(dir);
769
+ dirPath = File.join(@@cur_proj.baseDir, dir);
770
+ raise ArgumentError.new("Argument #1 is no directory: '#{dir}' ('#{dirPath}')") if (!Dir.exists?(dirPath));
771
+ @@cur_proj.inc_dirs.push(dirPath);
288
772
  elsif (dir.class == Array) then
289
773
  dir.each { |d| incDir(d) }
290
774
  else
291
- raise ArgumentError.new("argument #1 must be a String or an Array");
775
+ raise ArgumentError.new("Argument #1 must be a String or an Array");
292
776
  end
293
777
  more.each { |d| incDir(d) }
294
778
  end
295
779
 
296
- # adds source directorys to use for the current project
780
+ # Adds include directorys to use for projects that depends on the current project
297
781
  #
298
- # @note should only be called inside the block for {#newProject}
299
- # @param dir [String, Array<String>] directory path; root of the source path. if its a Array, each element is used in a call to {#srcDir}
300
- # @param more [Array<String>] additional source paths; each element is used in a call to {#srcDir}
782
+ # @note Should only be called inside the block for {#newProject}
783
+ # @param dir [String, Array<String>] Directory path; root of the include path. If its a Array, each element is used in a call to {#publish_incDir}
784
+ # @param more [Array<String>] Additional include paths; each element is used in a call to {#publish_incDir}
785
+ def publish_incDir(dir, *more)
786
+ if (@@cur_proj.libType == nil) then
787
+ raise RuntimeError.new("Can only be called when the project is configured as an library");
788
+ end
789
+
790
+ if (dir.class == String) then
791
+ dirPath = File.join(@@cur_proj.baseDir, dir);
792
+ raise ArgumentError.new("Argument #1 is no directory: '#{dir}' ('#{dirPath}')") if (!Dir.exists?(dirPath));
793
+ @@cur_proj.public_inc_dirs.push(dirPath);
794
+ elsif (dir.class == Array) then
795
+ dir.each { |d| publish_incDir(d) }
796
+ else
797
+ raise ArgumentError.new("Argument #1 must be a String or an Array");
798
+ end
799
+ more.each { |d| publish_incDir(d) }
800
+ end
801
+
802
+ # Adds source directorys to use for the current project
803
+ #
804
+ # @note Should only be called inside the block for {#newProject}
805
+ # @param dir [String, Array<String>] Directory path; root of the source path. If its a Array, each element is used in a call to {#srcDir}
806
+ # @param more [Array<String>] Additional source paths; each element is used in a call to {#srcDir}
301
807
  def srcDir(dir, *more)
302
808
  if (dir.class == String) then
303
- raise ArgumentError.new("argument #1 is no directory: '#{dir}'") if (!Dir.exists?(dir));
304
- @@cur_proj.src_dirs.push(dir);
809
+ dirPath = File.join(@@cur_proj.baseDir, dir);
810
+ raise ArgumentError.new("Argument #1 is no directory: '#{dir}' ('#{dirPath}')") if (!Dir.exists?(dirPath));
811
+ @@cur_proj.src_dirs.push(dirPath);
305
812
  elsif (dir.class == Array) then
306
813
  dir.each { |d| srcDir(d) }
307
814
  else
308
- raise ArgumentError.new("argument #1 must be a String or an Array");
815
+ raise ArgumentError.new("Argument #1 must be a String or an Array");
309
816
  end
310
817
  more.each { |d| srcDir(d) }
311
818
  end
312
819
 
313
- # tells the current project that it is not install-able
314
- # this means that if you run an install of your projects, this one will not be installed
820
+ # Adds a source glob to use for the current project
315
821
  #
316
- # @note should only be called inside the block for {#newProject}
822
+ # @note Should only be called inside the block for {#newProject}
823
+ # @param glob [String, Array<String>] Glob to be used to find source files; If its a Array, each element is used in a call to {#srcGlob}
824
+ # @param more [Array<String>] Additional source globs; each element is used in a call to {#srcGlob}
825
+ def srcGlob(glob, *more)
826
+ if (glob.class == String) then
827
+ @@cur_proj.src_globs.push(File.join(@@cur_proj.baseDir, glob));
828
+ elsif (glob.class == Array) then
829
+ glob.each { |g| srcGlob(g) }
830
+ else
831
+ raise ArgumentError.new("Argument #1 must be a String or an Array");
832
+ end
833
+ more.each { |g| srcGlob(g) }
834
+ end
835
+
836
+ # Tells the current project that it is not install-able
837
+ # This means that if you run an install of your projects, this one will not be installed
838
+ #
839
+ # @note Should only be called inside the block for {#newProject}
317
840
  def noInstall()
318
841
  @@cur_proj.no_install = true;
319
842
  end
320
843
 
321
- # sets the current project as a lib
844
+ # Sets the current project as a lib
322
845
  #
323
- # @note should only be called inside the block for {#newProject}
324
- # @param type [:static, :dynamic, :s, :dyn] the type of the library: static (*.a / *.lib) or dynamic (*.so / *.dll)
846
+ # @note Should only be called inside the block for {#newProject}
847
+ # @param type [:static, :dynamic, :s, :dyn, :both] The type of the library: static (*.a / *.lib) or dynamic (*.so / *.dll)
325
848
  def isLib(type)
326
849
  @@cur_proj.libType = :static if ([:static, :s].include?(type));
327
850
  @@cur_proj.libType = :dynamic if ([:dynamic, :dyn, :d].include?(type));
851
+ @@cur_proj.libType = :both if (type == :both);
852
+ raise ArgumentError.new("Argument #1 (type) must be one of the following: :static, :dynamic, :s, :dyn, :both") if (@@cur_proj.libType == nil)
328
853
  end
329
854
 
330
- # links a RBuildSys project to the current project
331
- # this has the effect that the given project is build before the current project, and
855
+ # Returns the install path for the current project
856
+ #
857
+ # @note Should only be called inside the block for {#newProject}
858
+ def getInstallPath()
859
+ dir = OPTIONS[:installDir] || "/usr/include" if (isLinux? || isMac?);
860
+ dir = OPTIONS[:installDir] || "C:/Program Files/#{@@cur_proj.outputName}" if (isWindows?);
861
+ return File.expand_path(dir);
862
+ end
863
+
864
+ # Links a RBuildSys project to the current project.
865
+ # This has the effect that the given project is build before the current project, and
332
866
  # if the given project is a lib, it is also linked to the current project
333
867
  #
334
- # @note should only be called inside the block for {#newProject}
335
- # @param name [String] can be a local project (inside current file) or any installed project
336
- def use(name)
868
+ # @note Should only be called inside the block for {#newProject}
869
+ # @param name [String] Can be a local project (inside current file) or any installed project
870
+ # @param linktype [:static, :dynamic] Specify wich linkage type should be used to link the project with the current one.
871
+ # If the project to link dosnt support the linktype, this raises an error.
872
+ def use(name, linktype)
337
873
  if (PROJECTS[name] == nil) then
338
- # use installed project
339
- raise NotImplementedError.new("");
874
+ @@cur_proj.dependencys.push([name, linktype]);
340
875
  else
341
876
  # use local
342
877
  proj = PROJECTS[name];
878
+
879
+ if (!proj.libType) then
880
+ raise ArgumentError.new("Argument #1 can't be used cause it is not a library");
881
+ end
882
+ if (proj.libType != linktype && proj.libType != :both) then
883
+ raise ArgumentError.new("Argument #1 can't be linked with linktype #{linktype}!");
884
+ end
885
+
886
+ @@cur_proj.dependencys.push(proj);
887
+ end
888
+ end
889
+
890
+ # Adds a external library through pkg-config.
891
+ # If no pkg-config is installed on the system, this functions raises an error
892
+ #
893
+ # @note Should only be called inside the block for {#newProject}
894
+ # @note For retriving the flags from pkg-config, +--libs+ and +--cflags+ is used, and the output is given to {#flag}
895
+ # @param name [String] Name of the pkg-config package
896
+ def usePkgconf(name)
897
+ if (`which pkg-config` == "") then
898
+ raise ArgumentError.new("Option pkgconf specified, but no pkg-config installed on system!");
343
899
  end
344
900
 
345
- if (!proj.libType) then
346
- raise ArgumentError.new("argument #1 can't be used cause it is not a library");
901
+ modver = `pkg-config --short-errors --modversion #{name}`;
902
+ if (modver.start_with?("No package") or $?.exitstatus != 0) then
903
+ raise RuntimeError.new("Could not find package '#{name}' with pkg-config!");
347
904
  end
348
905
 
349
- @@cur_proj.dependencys.push(proj);
906
+ linker_flags = `pkg-config --short-errors --libs #{name}`;
907
+ if ($?.exitstatus != 0) then
908
+ raise RuntimeError.new("Could not get linkler flags for '#{name}', plg-config error: #{linker_flags}");
909
+ end
910
+ flag(linker_flags);
911
+
912
+ cflags = `pkg-config --short-errors --cflags #{name}`;
913
+ if ($?.exitstatus != 0) then
914
+ raise RuntimeError.new("Could not get compiler flags for '#{name}', plg-config error: #{linker_flags}");
915
+ end
916
+ flag(cflags);
350
917
  end
351
918
 
352
- # adds a external library to the current project
919
+ # Adds a external library to the current project
353
920
  #
354
- # @note should only be called inside the block for {#newProject}
355
- # @note there is currently no check in place to verify that the given library exists
356
- # @param name [String] name of the library that should be used. in a c/c++ context, it should be without the leading "lib"
357
- # @param path [String, nil] optional search path for this library
358
- def useLib(name, path = nil)
921
+ # @note Should only be called inside the block for {#newProject}
922
+ # @note There is currently no check in place to verify that the given library exists
923
+ # @param name [String] Name of the library that should be used. in a c/c++ context, it should be without the leading "lib"
924
+ # @param path [String, nil] Optional search path for this library
925
+ # @param options [Hash] Optional options
926
+ # @option options :pkgconf [Boolean] If true, search pkgconf(ig) for additional informations about the lib
927
+ def useLib(name, path = nil, options = {})
928
+ if (options[:pkgconf]) then
929
+ usePkgconf(name);
930
+ return;
931
+ end
932
+
359
933
  @@cur_proj.librarys.push(name);
360
934
  @@cur_proj.lib_dirs.push(path) if (path != nil && Dir.exists?(path));
361
935
  end
362
936
 
363
- # adds an option to the option parser of RBuildSys
937
+ # Adds a define to the compiler call
938
+ #
939
+ # @note Should only be called inside the block for {#newProject}
940
+ # @param symbol [String] Symbol that should be defined
941
+ # @param value [String, nil] Optional value the symbol should hold
942
+ def define(symbol, value = nil)
943
+ @@cur_proj.defines[symbol] = value;
944
+ end
945
+
946
+ # Adds a flag to the flags of the current project.
947
+ #
948
+ # @note Should only be called inside the block for {#newProject}
949
+ # @note This should only be used when nothing else works, because this method dosnt ensure that the flags used are working with the current toolchain!
950
+ # @param flag [String] the flag to be added
951
+ def flag(flag)
952
+ @@cur_proj.flags.push(flag);
953
+ end
954
+
955
+ #########################################################################################
956
+ # Option parsing #
957
+ #########################################################################################
958
+
959
+ # Adds an option to the option parser of RBuildSys
364
960
  # @see OptionParser#on
365
961
  def onOption(*opts, &block)
366
962
  @@optparse.on(*opts, block);
367
963
  end
368
964
 
369
- # adds an option to the option parser of RBuildSys
965
+ # Adds an option to the option parser of RBuildSys
370
966
  # @see OptionParser#on_tail
371
967
  def onTailOption(*opts, &block)
372
968
  @@optparse.on_tail(*opts, block);
373
969
  end
374
970
 
375
- # adds an option to the option parser of RBuildSys
971
+ # Adds an option to the option parser of RBuildSys
376
972
  # @see OptionParser#on_head
377
973
  def onHeadOption(*opts, &block)
378
974
  @@optparse.on_head(*opts, block);
379
975
  end
380
976
 
381
- # adds a flag to the flags of the current project.
977
+ #########################################################################################
978
+ # Toolchains #
979
+ #########################################################################################
980
+
981
+ # Loads a toolchain definition from a file. Format needs to be JSON.
382
982
  #
383
- # @note should only be called inside the block for {#newProject}
384
- # @param flag [String] the flag to be added
385
- def flag(flag)
386
- @@cur_proj.flags.push(flag);
387
- end
983
+ # @param file [String] path to the toolchain definition file
984
+ def self.loadToolchain(file)
985
+ if (!File.exists?(file)) then
986
+ raise ArgumentError.new("Argument #1 (file) needs to be the path to an existing file");
987
+ end
388
988
 
389
- # begins the build process.
390
- # should be called after you configured all your projects with {#newProject}.
391
- def build()
392
- @@optparse.parse!
393
- if (ARGV.length == 0) then
394
- puts @@optparse;
395
- exit(1);
989
+ data = JSON.parse(File.read(file));
990
+ if (TOOLCHAINS.keys.include?(data["name"])) then
991
+ raise RuntimeError.new("Cannot load toolchain '#{data["name"]}' from '#{file}': already loaded");
396
992
  end
993
+ TOOLCHAINS[data["name"]] = data;
397
994
 
398
- ARGV.each { |projname|
399
- proj = PROJECTS[projname];
400
- if (proj) then
401
- if (OPTIONS[:clean]) then
402
- proj.clean();
403
- else
404
- proj.build();
405
- end
406
- end
995
+ return data;
996
+ end
997
+
998
+ private
999
+ def self.load_default_toolchains
1000
+ # load builtin toolchains
1001
+ gemDir = File.dirname(File.expand_path(__FILE__));
1002
+ toolchains = Dir.glob("#{gemDir}/toolchains/*.json");
1003
+ toolchains.each {|f|
1004
+ self.loadToolchain(f);
407
1005
  }
1006
+
1007
+ # use toolchain installed for the user
1008
+ toolchains = Dir.glob("#{Dir.home}/.rbuildsys/toolchains/*.json");
1009
+ toolchains.each {|f|
1010
+ self.loadToolchain(f);
1011
+ }
1012
+ end
1013
+ load_default_toolchains()
1014
+
1015
+ #########################################################################################
1016
+ # Configure files #
1017
+ #########################################################################################
1018
+
1019
+ public
1020
+ # Sets a symbol with an optional value to configure files. See {#configureFile}
1021
+ #
1022
+ # @note When used inside the block for {#newProject}, the symbol is only visible for the project. If used outside of the block, the symbol is visible for everyone!
1023
+ # @param symbol [String] The symbol that should be set
1024
+ # @param value [String, nil] Optional value for the symbol
1025
+ def set(symbol, value = nil)
1026
+ if (@@cur_proj == nil) then
1027
+ CONFIG_SYMBOLS[symbol] = value;
1028
+ else
1029
+ @@cur_proj.config_symbols[symbol] = value;
1030
+ end
1031
+ end
1032
+
1033
+ # Tells the project that, in order to build it, the file specified at +input+ must be transformed to the file at +output+.
1034
+ #
1035
+ # @note Should only be called inside the block for {#newProject}
1036
+ # @param input [String] The filename of the file that should be configured
1037
+ # @param output [String] The filename that should be used to save the result, cannot be the same as the input!
1038
+ # @param options [Hash] Some optional options
1039
+ def configureFile(input, output, options = {})
1040
+ if (!input.is_a?(String) || input.strip.empty?) then
1041
+ raise ArgumentError.new("Argument #1 (input) needs to be a string");
1042
+ end
1043
+
1044
+ if (!output.is_a?(String) || output.strip.empty?) then
1045
+ raise ArgumentError.new("Argument #2 (output) needs to be a string");
1046
+ end
1047
+
1048
+ if (input == output) then
1049
+ raise ArgumentError.new("Argument #1 (input) and #2 (output) cannot be the same");
1050
+ end
1051
+
1052
+ @@cur_proj.config_files.push({
1053
+ input: File.join(@@cur_proj.baseDir, input),
1054
+ output: File.join(@@cur_proj.baseDir, output),
1055
+ options: options
1056
+ });
1057
+ end
1058
+
1059
+ #########################################################################################
1060
+ # Utils #
1061
+ #########################################################################################
1062
+
1063
+ # Shorthand for {Project#isWindows?}
1064
+ def isWindows?()
1065
+ return @@cur_proj.isWindows?;
1066
+ end
1067
+
1068
+ # Shorthand for {Project#isMac?}
1069
+ def isMac?()
1070
+ return @@cur_proj.isMac?;
1071
+ end
1072
+
1073
+ # Shorthand for {Project#isMac?}
1074
+ def isLinux?()
1075
+ return @@cur_proj.isLinux?;
1076
+ end
1077
+
1078
+ # Tests if the toolchain is the given type of OS.
1079
+ #
1080
+ # @param type [:win, :win32, :windows, :mac, :macos, :apple, :linux] the OS to check
1081
+ # @return [Boolean] Returns true if OS is correct, false otherwise
1082
+ def isToolchainOS?(type)
1083
+ if ([:win, :win32, :windows].include?(type)) then
1084
+ return @@cur_proj.isWindows?;
1085
+ end
1086
+
1087
+ if ([:mac, :macos, :apple].include?(type)) then
1088
+ return @@cur_proj.isMac?();
1089
+ end
1090
+
1091
+ if ([:linux].include?(type)) then
1092
+ return @@cur_proj.isLinux?();
1093
+ end
1094
+
1095
+ raise ArgumentError.new("Unexpected argument: os-type '#{type}' unknown");
408
1096
  end
409
1097
 
410
1098
  end