class RBuildSys::Project
Class to describe a buildable RBuildSys
project
@author Mai-Lapyst
Attributes
Returns the base directory for this project @return [String]
Returns the config files that should be configured @return [Array<Hash>]
Returns the defined symbols and its value for config files for this project @return [Hash]
Returns the defined symbols and its value for this project @return [Hash]
Returns the dependencys of this project @return [Array<Project>]
Returns the array of flags for this project; will be added to the compiler call @return [Array<String>]
Returns the array of include directorys for this project @return [Array<String>]
Returns the library type of this project; if nil, this project isn't a library. :both
is when the project can be static AND dynamic without any changes inside the sourcecode. @return [:static, :dynamic, :s, :dyn, :both]
Returns the array of library directorys for this project @return [Array<String>]
Returns the array of librarys for this project @return [Array<String>]
Returns the name of the project @return [String]
Returns true if the project is install-able, false otherwise @return [true, false]
Returns the output name of the project; typically the same as {#name} @return [String]
Returns the array of “public” include directorys for this project. These will only be used by projects that depends on this project @return [Array<String>]
Returns the array of source directorys for this project @return [Array<String>]
Returns the array of source globs for this project @return [Array<String>]
Returns the toolchain used for this project @return [Hash]
Public Class Methods
Initializes a new instance of this class. Default language is “c”.
@param name [String] Name of the project; see {#name} @param options [Hash] Various options for the project @option options :lang [Symbol] Language used for this project. Available: :c
, :cpp
@option options :toolchain [String] Toolchain that should be used; if no is supplied, “gnu” is used. @option options :srcFile_endings [Array<String>] Additional fileendings that should be used for finding sourcefiles @option options :no_install [true, false] Flag that tells if the project is install-able or not; see {#no_install} @option options :libType [:static, :dynamic, :s, :dyn, :both, nil] Type of library; see {#libType} @option options :outputName [String] If the output name shoud differ from the project name, specify it here @option options :baseDir [String] Directory that should be the base of the project; doesn't change the build directory
# File lib/rbuildsys.rb, line 94 def initialize(name, options = {}) if (!options.is_a?(Hash)) then raise ArgumentError.new("Argument #2 (options) need to be an hash!"); end @name = name; @src_dirs = []; @src_globs = []; @inc_dirs = []; @public_inc_dirs = []; @lib_dirs = []; @dependencys = []; @librarys = []; @flags = []; @defines = {}; @config_symbols = {}; @config_files = []; check_lang(options[:lang] || "c"); if (OPTIONS[:toolchainOverride]) then if (!load_and_check_toolchain(OPTIONS[:toolchainOverride])) then raise RuntimeError.new("Commandline specified a toolchain override, but toolchain cannot be found: '#{@toolchain_name}'"); end else if (!load_and_check_toolchain(options[:toolchain] || "gnu")) then raise ArgumentError.new("Argument #2 (options) contains key toolchain, but toolchain cannot be found: '#{@toolchain_name}'"); end end @src_file_endings = []; @src_file_endings = ["cpp", "cc", "c++", "cxx"] if (@lang == :cpp); @src_file_endings = ["c"] if (@lang == :c); if (options[:srcFile_endings]) then @src_file_endings.push(*options[:srcFile_endings]); @src_file_endings.uniq!; end @no_install = options[:no_install] || false; @libType = options[:libType]; @outputName = options[:outputName] || @name; @baseDir = options[:baseDir] || "."; end
Public Instance Methods
Builds the project @return [true, false] State of the build, true means success, false means failure
# File lib/rbuildsys.rb, line 312 def build() for dep in @dependencys do next if (OPTIONS[:cleanBuild]); if (dep.is_a?(Project) && !dep.build()) then return false; end end puts "Now building '#{@name}'"; puts "- using language #{@lang}"; checkDependencys(); #pp @dependencys.map{|dep| if (dep.is_a?(Project)) then "local #{dep.name}" else "installed #{dep["name"]}" end } # TODO: we dont check if previous build files where compiled with the current toolchain or not! for config_file in @config_files do configure_file(config_file[:input], config_file[:output], config_file[:options]); end for dep in @dependencys do if (dep.is_a?(Project)) then @inc_dirs.push(*dep.public_inc_dirs).uniq!; @lib_dirs.push("./build/#{dep.name}"); @librarys.push(dep.name); if (dep.libType == :static) then @lib_dirs.push(*dep.lib_dirs).uniq! @librarys.push(*dep.librarys).uniq! end elsif (dep.is_a?(Hash)) then # dependency is an globaly installed library @inc_dirs.push(File.join(getInstallPath(), "include", dep["name"])).uniq!; @lib_dirs.push(File.join(getInstallPath(), "lib")).uniq!; @librarys.push(dep["name"]); if (dep["libType"] == "static") then @lib_dirs.push(*dep["lib_dirs"]).uniq!; @librarys.push(*dep["librarys"]).uniq!; end end end @flags.push(@toolchain["flags"]["debug"]) if (OPTIONS[:debug]); @flags.push(@toolchain["extra_flags"]) if (!@toolchain["extra_flags"].strip.empty?); # create the project build dir buildDir = "./build/#{@name}"; FileUtils.mkdir_p(buildDir); # make the includes #inc_dirs.map! { |incDir| incDir + "/*.h" } #includes = Dir.glob(inc_dirs); includes = @inc_dirs.map{|incDir| "#{@toolchain["flags"]["include"]} #{incDir}"}.join(" "); libs = @lib_dirs.map{|libDir| "#{@toolchain["flags"]["libPath"]} #{libDir}"}.join(" ") + " " + @librarys.map{|lib| "#{@toolchain["flags"]["libLink"]} #{lib}"}.join(" "); std = @c_standard ? ("#{@toolchain["flags"]["langStd"]}=#{@c_standard}") : ""; defines = @defines.map{|sym,val| if val == nil then "#{@toolchain["flags"]["define"]}#{sym}" else "#{@toolchain["flags"]["define"]}#{sym}=#{val}" end }.join(" "); # for now, just ignore all global and relative paths for sources src_dirs.select! { |srcDir| srcDir[0] != "/" && !srcDir.start_with?("../") } source_modified = false; build_failed = false; build_file = ->(srcFile, srcDir) { #objFile = File.join(buildDir, srcFile.gsub(srcDir, "")); objFile = File.join(buildDir, srcFile); objFile = objFile.gsub(Regexp.new("\.(#{@src_file_endings.join("|")})$"), ".o"); srcTime = File.mtime(srcFile); if (OPTIONS[:cleanBuild] || OPTIONS[:cleanBuildAll] || !File.exists?(objFile) || (srcTime > File.mtime(objFile))) then source_modified = true; FileUtils.mkdir_p(File.dirname(objFile)); # ensure we have the parent dir(s) # build the source! cmd = "#{@compiler}" cmd += " #{std}" if (!std.empty?); cmd += " #{@toolchain["flags"]["pic"]}" if (@libType == :dynamic || @libType == :both); cmd += " #{includes}"; cmd += " #{defines}" if (defines.size > 0); cmd += " #{@flags.join(" ")}" if (@flags.size > 0); cmd += " -c #{@toolchain["flags"]["output"]} #{objFile} #{srcFile}"; puts "- $ #{cmd}"; f = system(cmd); if (f) then FileUtils.touch(objFile, :mtime => srcTime); else build_failed = true; end end } src_dirs.each { |srcDir| globStr = File.join(srcDir, "**/*.{#{@src_file_endings.join(",")}}"); srcFiles = Dir.glob(globStr); srcFiles.each { |srcFile| build_file.call(srcFile, srcDir) } } src_globs.each{ |glob| srcFiles = Dir.glob(glob); srcFiles.each { |srcFile| build_file.call(srcFile, File.dirname(srcFile)) } } if (build_failed) then puts "Build failed, see log for details!"; return false; end objFiles = Dir.glob(buildDir + "/**/*.o"); if (@libType != nil) then f = true; if (@libType == :static || @libType == :both) then libname = @toolchain["output_filenames"]["staticLib"].clone; libname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName); libpath = File.join(buildDir, libname); if (!File.exists?(libpath) || source_modified) then puts "- Building static library #{libname}!"; cmd = "#{@archiver} rcs #{libpath} #{objFiles.join(" ")}"; puts " - $ #{cmd}"; f_static = system(cmd); f = false if (!f_static); else puts "- No need for building library, nothing changed!"; end end if (@libType == :dynamic || @libType == :both) then #libname = @toolchain["output_filenames"]["dynamicLib"].clone; #libname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName); #puts "- Building dynamic library #{libname}!"; #libname = File.join(buildDir, "lib#{name}.so"); #cmd = "" puts "[WARN] dynamic librarys are not implemented yet!"; end metadata = { name: @outputName, libType: @libType, lib_dirs: @lib_dirs, librarys: @librarys, dependencys: @dependencys.map {|dep| if (dep.is_a?(Project)) then dep.outputName else dep["name"]; end }, toolchain: @toolchain_name }; # TODO: copy the toolchain definition, if the toolchain is not permanently installed on the system! metadataFile = File.join(buildDir, "#{@outputName}.config.json"); File.write(metadataFile, JSON.pretty_generate(metadata)); return f; else # build runable binary binname = @toolchain["output_filenames"]["exec"].clone; binname.gsub!(/\@[Nn][Aa][Mm][Ee]\@/, @outputName); binpath = File.join(buildDir, binname); if (!File.exists?(binpath) || source_modified) then puts "- Building executable #{binname}!"; cmd = "#{@compiler} #{includes}" cmd += " #{@flags.join(" ")}" if (@flags.size > 0) cmd += " #{@toolchain["flags"]["output"]} #{binpath} #{objFiles.join(" ")} #{libs}"; puts " - $ #{cmd}"; f = system(cmd); return f; else puts "- No need for building binary, nothing changed!"; end end return true; end
Cleans the project's output directory
# File lib/rbuildsys.rb, line 195 def clean() buildDir = "./build/#{name}"; FileUtils.remove_dir(buildDir) if File.directory?(buildDir) end
Configure a specific file
@param input [String] The filename of the file that should be configured @param output [String] The filename that should be used to save the result, cannot be the same as the input! @param options [Hash] Some optional options @option options :realUndef [Boolean] If this is true, not defined symbols will be undefined with '#undef <symbol>'
# File lib/rbuildsys.rb, line 219 def configure_file(input, output, options = {}) # based on https://cmake.org/cmake/help/latest/command/configure_file.html puts("- configure: #{input}"); if (input.is_a?(String)) then data = File.read(input); end # replace cmake defines cmake_defines = data.to_enum(:scan, /\#cmakedefine ([a-zA-Z0-9_]+)([^\n]+)?/).map { Regexp.last_match }; for cmake_def in cmake_defines do if (hasSymbol(cmake_def[1])) then data.sub!(cmake_def[0], "#define #{cmake_def[1]}"); else if (options[:realUndef]) then data.sub!(cmake_def[0], "#undef #{cmake_def[1]}"); else data.sub!(cmake_def[0], "/* #undef #{cmake_def[1]} */"); end end end # replace variables! matches = data.to_enum(:scan, /\@([a-zA-Z0-9_]+)\@/).map { Regexp.last_match }; for match in matches do if (hasSymbol(match[1])) then data.sub!(match[0], getSymbolValue(match[1]).inspect); else puts "[WARN] in file #{input}: #{match[0]} found, but no value for it defined!"; end end File.write(output, data); end
Installs the project
# File lib/rbuildsys.rb, line 505 def install() dir = (File.expand_path(OPTIONS[:installDir]) || "/usr/local") if (isLinux? || isMac?) dir = (File.expand_path(OPTIONS[:installDir]) || "C:/Program Files/#{@outputName}") if (isWindows?) puts "RBuildSys will install #{@name} to the following location: #{dir}"; puts "Do you want to proceed? [y/N]: " if ( STDIN.readline.strip != "y" ) then puts "Aborting installation..."; exit(1); end # 1. install all includes incDir = File.join(dir, "include", @outputName); if (!Dir.exists?(incDir)) then puts "- create dir: #{incDir}"; FileUtils.mkdir_p(incDir); end @public_inc_dirs.each {|d| files = Dir.glob(File.join("#{d}", "**/*.{h,hpp}")); files.each {|f| dest = File.join(incDir, f.gsub(d, "")); puts "- install: #{dest}"; destDir = File.dirname(dest); if (!Dir.exists?(destDir)) then puts "- create dir: #{destDir}"; FileUtils.mkdir_p(destDir); end FileUtils.copy_file(f, dest) } } # 2.1 install library results (if any) buildDir = "./build/#{@name}"; if (@libType) then libDir = File.join(dir, "lib"); if (!Dir.exists?(libDir)) then puts "- create dir: #{libDir}"; FileUtils.mkdir_p(libDir); end files = Dir.glob(File.join(buildDir, "*.{a,lib,so,dll}")); # TODO: this glob should based on the toolchain definition files.each {|f| dest = File.join(libDir, f.gsub(buildDir, "")); puts "- install: #{dest}"; FileUtils.copy_file(f, dest) } # install definitions so we can use them in other projects easier! metadataDir = File.join(libDir, "rbuildsys_conf"); if (!Dir.exists?(metadataDir)) then puts "- create dir: #{metadataDir}"; FileUtils.mkdir_p(metadataDir); end metadataFile = File.join(metadataDir, "#{@outputName}.config.json"); puts "- install: #{metadataFile}"; FileUtils.copy_file(File.join(buildDir, "#{@outputName}.config.json"), metadataFile); end # 2.2 install executable results if (!@libType) then raise NotImplementedError.new("installation of executables (*.exe, *.run etc.) is not supported yet"); end end
Tests if the toolchain for this project is for linux.
@return [Boolean] Returns true if linux, false otherwise
# File lib/rbuildsys.rb, line 275 def isLinux?() return @toolchain["os"] == "linux"; end
Tests if the toolchain for this project is for macos.
@return [Boolean] Returns true if macos, false otherwise
# File lib/rbuildsys.rb, line 267 def isMac?() return @toolchain["os"] == "macos"; end
Tests if the toolchain for this project is for windows.
@return [Boolean] Returns true if windows, false otherwise
# File lib/rbuildsys.rb, line 259 def isWindows?() return @toolchain["os"] == "windows"; end
Private Instance Methods
# File lib/rbuildsys.rb, line 280 def checkDependencys() @dependencys.each_index {|idx| dep = @dependencys[idx]; if (dep.is_a?(Array)) then name = dep[0]; # use installed project proj_conf_path = File.join(getInstallPath(), "lib", "rbuildsys_conf", "#{name}.config.json"); if (!File.exists?(proj_conf_path)) then raise RuntimeError.new("Could not find project '#{name}'!"); end proj_conf = JSON.parse(File.read(proj_conf_path)); if (!["static", "dynamic", "both"].include?(proj_conf["libType"])) then raise RuntimeError.new("'#{name}' can't be used as a dependency because it is not a library"); end if (proj_conf["libType"] != dep[1].to_s || proj_conf["libType"] == "both") then raise RuntimeError.new("'#{name}' can't be linked with linktype #{dep[1]}"); end if (proj_conf["toolchain"] != @toolchain_name) then raise RuntimeError.new("Dependency '#{name}' was compiled using the toolchain #{proj_conf["toolchain"]}, while this project trys to use #{@toolchain_name}!"); end @dependencys[idx] = proj_conf; end } end
# File lib/rbuildsys.rb, line 157 def check_binary(bin) `which #{bin}`; raise RuntimeError.new("Trying to use binary '#{bin}', but binary dosnt exists or is not in PATH") if ($?.exitstatus != 0); end
lang must be a valid string for the gcc/g++ -std option: gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html or simply the language: c, c++, gnu, gnu++
# File lib/rbuildsys.rb, line 166 def check_lang(lang) if ([:c, :cpp].include?(lang)) then @lang = lang; return; end if ((m = lang.match(/^(c\+\+|gnu\+\+)([\dxyza]+)?$/)) != nil) then @lang = :cpp; @c_standard = lang if (m[2]); return; end if ((m = lang.match(/^(c|gnu)([\dx]+)?$/)) != nil) then @lang = :c; @c_standard = lang if (m[2]); return; end if (lang.match(/^iso(\d+):([\dx]+)$/) != nil) then @lang = :c; @c_standard = lang; return; end raise ArgumentError.new("Initializer argument #2 (options) contains key lang, but cannot validate it: '#{lang}'"); end
# File lib/rbuildsys.rb, line 207 def getSymbolValue(sym) return @config_symbols[sym] if (@config_symbols.keys.include?(sym)); return CONFIG_SYMBOLS[sym]; end
# File lib/rbuildsys.rb, line 201 def hasSymbol(sym) return true if (@config_symbols.keys.include?(sym)); return CONFIG_SYMBOLS.keys.include?(sym); end
# File lib/rbuildsys.rb, line 139 def load_and_check_toolchain(name) @toolchain_name = name; @toolchain = TOOLCHAINS[@toolchain_name]; if (!@toolchain) then return false; end @compiler = @toolchain["compiler"]["c"] if (@lang == :c); @compiler = @toolchain["compiler"]["c++"] if (@lang == :cpp); check_binary(@compiler); @archiver = @toolchain["archiver"]; check_binary(@archiver); return true; end