class RBuildSys::Project

Class to describe a buildable RBuildSys project

@author Mai-Lapyst

Attributes

baseDir[RW]

Returns the base directory for this project @return [String]

config_files[R]

Returns the config files that should be configured @return [Array<Hash>]

config_symbols[R]

Returns the defined symbols and its value for config files for this project @return [Hash]

defines[R]

Returns the defined symbols and its value for this project @return [Hash]

dependencys[R]

Returns the dependencys of this project @return [Array<Project>]

flags[R]

Returns the array of flags for this project; will be added to the compiler call @return [Array<String>]

inc_dirs[R]

Returns the array of include directorys for this project @return [Array<String>]

libType[RW]

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]

lib_dirs[R]

Returns the array of library directorys for this project @return [Array<String>]

librarys[R]

Returns the array of librarys for this project @return [Array<String>]

name[R]

Returns the name of the project @return [String]

no_install[RW]

Returns true if the project is install-able, false otherwise @return [true, false]

outputName[RW]

Returns the output name of the project; typically the same as {#name} @return [String]

public_inc_dirs[R]

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>]

src_dirs[R]

Returns the array of source directorys for this project @return [Array<String>]

src_globs[R]

Returns the array of source globs for this project @return [Array<String>]

toolchain[R]

Returns the toolchain used for this project @return [Hash]

Public Class Methods

new(name, options = {}) click to toggle source

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

build() click to toggle source

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
clean() click to toggle source

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_file(input, output, options = {}) click to toggle source

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
install() click to toggle source

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
isLinux?() click to toggle source

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
isMac?() click to toggle source

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
isWindows?() click to toggle source

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

checkDependencys() click to toggle source
# 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
check_binary(bin) click to toggle source
# 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
check_lang(lang) click to toggle source

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
getSymbolValue(sym) click to toggle source
# File lib/rbuildsys.rb, line 207
def getSymbolValue(sym)
    return @config_symbols[sym] if (@config_symbols.keys.include?(sym));
    return CONFIG_SYMBOLS[sym];
end
hasSymbol(sym) click to toggle source
# File lib/rbuildsys.rb, line 201
def hasSymbol(sym)
    return true if (@config_symbols.keys.include?(sym));
    return CONFIG_SYMBOLS.keys.include?(sym);
end
load_and_check_toolchain(name) click to toggle source
# 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