class Canoe::WorkSpace
A workspace resents a C/C++ project This class is responsible for the main functionality of canoe, such as building and cleaning TODO
add a command to generate compile_commands.json so users won't have to install bear
Attributes
components_prefix[R]
components_short[R]
cwd[R]
header_suffix[R]
mode[R]
name[R]
obj_prefix[R]
obj_short[R]
source_suffix[R]
src_prefix[R]
src_short[R]
target_short[R]
tests_short[R]
Public Class Methods
help()
click to toggle source
# File lib/workspace/help.rb, line 3 def self.help info = <<~INFO canoe is a C/C++ project manager, inspired by Rust cargo. usage: canoe new tada: create a project named 'tada' in current directory canoe build: compile current project (execute this command in project directory) canoe test: build and run tests canoe generate: generate dependency relationships and store it in '.canoe.deps' file. Alias: update canoe update: udpate dependency relationships and store it in '.canoe.deps' file. canoe run: compile and execute current project (execute this command in project directory) canoe clean: remove all generated object files and binary files canoe help: show this help message canoe add tada: add a folder named tada under workspace/components, canoe dep: show current dependency relationships of current project canoe verion: version information canoe make: generate a makefile for this project new project_name [mode] [suffixes]: create a new project with project_name. In this project, four directories obj, src, target and third-party will be generated in project directory. in src, directory 'components' will be generated if [mode] is '--lib', an extra main.cpp will be generated if [mode] is '--bin' [mode]: --lib for a library and --bin for executable binaries [suffixes]: should be in 'source_suffix:header_suffix" format, notice the ':' between two suffixes add component_name: add a folder named tada under workspace/components. two files tada.hpp and tada.cpp would be craeted and intialized. File suffix may differ according users' specifications. if component_name is a path separated by '/', then canoe would create folders and corresponding files recursively. generate: generate dependence relationship for each file, this may accelarate `canoe buid` command. It's recommanded to execute this command everytime headers are added or removed from any file. update: this command is needed because '.canoe.deps' is actually a cache of dependency relationships so that canoe doesn't have to analyze all the files when building a project. So when a file includes new headers or some headers are removed, users have to use 'canoe udpate' to update dependency relationships. build [all|test]: build current project, 'all' builds both target and tests, 'test' builds tests only test [tests] [args]: build and run tests [tests]: 'all' for all tests, or a name of a test for a single test [args]: args are passed to the single test run [options]: build current project with no specific compilation flags, and run this project, passing [options] as command line arguments to the binary clean: remove all generated object files and binary files help: show this help message verion: display version information dep: display file dependencies in a better readable way make: generate a Makefile for this project @author: written by XIONG Ziwei, ICT, CAS @contact: noahxiong@outlook.com INFO puts info end
new(name, mode, src_suffix = 'cpp', hdr_suffix = 'hpp', nu = false)
click to toggle source
# File lib/workspace/workspace.rb, line 23 def initialize(name, mode, src_suffix = 'cpp', hdr_suffix = 'hpp', nu = false) @name = name @compiler = Compiler.new 'clang++', ['-Isrc/components'], [] @cwd = Dir.new(Dir.pwd) @workspace = Dir.pwd.to_s + (nu ? "/#{@name}" : '') @src = "#{@workspace}/src" @components = "#{@src}/components" @obj = "#{@workspace}/obj" @third = "#{@workspace}/third-party" @target = "#{@workspace}/target" @tests = "#{@workspace}/tests" @mode = mode @deps = '.canoe.deps' @test_deps = '.canoe.test.deps' @target_short = './target' @src_short = './src' @components_short = "#{@src_short}/components" @obj_short = './obj' @tests_short = './tests' @src_prefix = './src/' @components_prefix = './src/components/' @obj_prefix = './obj/' @source_suffix = src_suffix @header_suffix = hdr_suffix end
version()
click to toggle source
# File lib/workspace/version.rb, line 3 def self.version puts <<~VER canoe v0.3.3.1 For features in this version, please visit https://github.com/Dicridon/canoe Currently, canoe can do below: - project creation - project auto build, run and test (works like Cargo for Rust) - project structure management by XIONG Ziwei VER end
Public Instance Methods
add(args)
click to toggle source
# File lib/workspace/add.rb, line 3 def add(args) args.each do |i| dir = @components filenames = i.split '/' prefix = [] filenames.each do |filename| dir += "/#{filename}" prefix << filename next if Dir.exist? dir FileUtils.mkdir dir Dir.chdir(dir) do puts "created + #{Dir.pwd.blue}" create_working_files prefix.join('__'), filename end end end end
build(arg = 'target')
click to toggle source
args are commandline parameters passed to `canoe build`, could be 'all', 'test', 'target', 'base' or empty
# File lib/workspace/build.rb, line 27 def build(arg = 'target') build_compiler_from_config send "build_#{arg}" end
clean(arg = 'all')
click to toggle source
valid options: none, 'all', 'target', 'tests'
# File lib/workspace/clean.rb, line 4 def clean(arg = 'all') send "clean_#{arg}" end
comp_to_obj(comp)
click to toggle source
# File lib/workspace/build.rb, line 8 def comp_to_obj(comp) @obj_prefix + comp.delete_suffix(File.extname(comp))[@components_prefix.length..].gsub("/", "_") + ".o" end
dep()
click to toggle source
# File lib/workspace/dep.rb, line 3 def dep deps = DepAnalyzer.read_from(@deps) if File.exist?(@deps) deps.each do |k, v| next if v.empty? puts "#{k.blue} depends on: " v.each { |f| puts " #{f.blue}" } puts '' end end
extract_one_file(file, deps)
click to toggle source
extract one test file's dependency
# File lib/workspace/test.rb, line 13 def extract_one_file(file, deps) ret = deps[file].map { |f| f.gsub(".#{@header_suffix}", ".#{@source_suffix}") } deps[file].each do |f| dep = extract_one_file(f, deps) dep.each do |d| ret << d unless ret.include?(d) end end ret.map { |f| f.gsub(".#{@header_suffix}", ".#{@source_suffix}") } end
extract_one_file_obj(file, deps)
click to toggle source
# File lib/workspace/test.rb, line 25 def extract_one_file_obj(file, deps) extract_one_file(file, deps).map do |f| file_to_obj(f) end end
file_to_obj(file)
click to toggle source
the if else order is important because tests are regarded as sources
# File lib/workspace/build.rb, line 13 def file_to_obj(file) if file.start_with?(@components_prefix) comp_to_obj file else src_to_obj file end end
generate()
click to toggle source
# File lib/workspace/generate.rb, line 3 def generate DepAnalyzer.new(@src_short, @source_suffix, @header_suffix) .build_to_file [@src_short, @components_short], @deps DepAnalyzer.new(@tests_short, @source_suffix, @header_suffix) .build_to_file [@src_short, @components_short], @test_deps end
hdr_of_src(file)
click to toggle source
# File lib/workspace/build.rb, line 21 def hdr_of_src(file) file.gsub(".#{@source_suffix}", ".#{@header_suffix}") end
make()
click to toggle source
# File lib/workspace/make.rb, line 269 def make config = ConfigReader.new('config.json').extract_flags deps = target_deps.merge tests_deps makefile = CanoeMakefile.new self makefile.configure config makefile.make! deps end
new()
click to toggle source
# File lib/workspace/new.rb, line 3 def new begin Dir.mkdir(@name) rescue SystemCallError abort_on_err "workspace #{@name} already exsits" end Dir.mkdir(@src) Dir.mkdir(@components) Dir.mkdir(@obj) add_gitignore @obj if @mode == :bin DefaultFiles.create_main(@src, @source_suffix) else DefaultFiles.create_lib_header(@src, @name, @header_suffix) end File.new("#{@workspace}/.canoe", 'w') compiler = @source_suffix == 'c' ? 'clang' : 'clang++' DefaultFiles.create_config @workspace, compiler, @source_suffix, @header_suffix Dir.mkdir(@third) Dir.mkdir(@target) add_gitignore @target Dir.mkdir(@tests) Dir.chdir(@workspace) do issue_command 'git init' issue_command 'canoe add tests' end puts "workspace #{@workspace.blue} is created" end
run(args)
click to toggle source
# File lib/workspace/run.rb, line 3 def run(args) return if @mode == :lib return unless build args = args.join ' ' run_command "#{@target_short}/#{@name} #{args}" end
src_to_obj(src)
click to toggle source
# File lib/workspace/build.rb, line 4 def src_to_obj(src) @obj_prefix + File.basename(src, ".*") + ".o" end
test(args)
click to toggle source
# File lib/workspace/test.rb, line 3 def test(args) if args.empty? test_all return end # we don't handle spaces test_single(args[0], args[1..].join(" ")) end
update()
click to toggle source
# File lib/workspace/update.rb, line 3 def update generate end
Private Instance Methods
add_gitignore(dir)
click to toggle source
# File lib/workspace/new.rb, line 35 def add_gitignore(dir) Dir.chdir(dir) do File.open('.gitignore', 'w') do |f| f.write "*\n!.gitignore\n" end end end
build_all()
click to toggle source
# File lib/workspace/build.rb, line 114 def build_all build_target build_test end
build_base()
click to toggle source
generate a compile_commands.json file
# File lib/workspace/build.rb, line 149 def build_base deps = target_deps.merge tests_deps database = CompilationDatabase.new deps.each_key do |k| next if k.end_with? @header_suffix c = @compiler.name.end_with?('++') ? 'c++' : 'c' arg = [c] + @compiler.compiling_flags_as_str.split + [k] + [file_to_obj(k)] + ['-c', '-o'] database.add_command_object(@workspace, arg, k) end File.open('compile_commands.json', 'w') do |f| f.puts database.pretty_to_s end end
build_bin(files)
click to toggle source
# File lib/workspace/build.rb, line 78 def build_bin(files) if build_common(files) && link_exectutable(@target_short, Dir.glob("obj/*.o").reject { |f| f.start_with? 'obj/test_' }) puts "BUILDING SUCCEEDED".green return true else puts "building target FAILED".red return false end end
build_common(files)
click to toggle source
# File lib/workspace/build.rb, line 99 def build_common(files) all = SourceFiles.get_all(@src_short) { |f| f.end_with? @source_suffix } stepper = Stepper.new all.size, files.size flag = true files.each do |f| progress = stepper.progress_as_str.green printf "#{progress.green} compiling #{f.yellow}: " o = file_to_obj(f) flag = false unless compile f, o stepper.step end flag end
build_compiler_from_config()
click to toggle source
# File lib/workspace/build.rb, line 49 def build_compiler_from_config flags = ConfigReader.new('config.json').extract_flags compiler_name = flags['compiler'] ? flags['compiler'] : 'clang++' abort_on_err "compiler #{compiler_name} not found" unless system "which #{compiler_name} > /dev/null" compiler_flags = ['-Isrc/components'] linker_flags = [] c_flags, l_flags = flags['flags']['compile'], flags['flags']['link'] build_flags(compiler_flags, c_flags) build_flags(linker_flags, l_flags) @compiler = Compiler.new compiler_name, compiler_flags, linker_flags end
build_flags(flags, config)
click to toggle source
# File lib/workspace/build.rb, line 34 def build_flags(flags, config) config.values.each do |v| case v when String flags << v when Array v.each do |o| flags << o end else abort_on_err "unknown options in config.json, #{v}" end end end
build_lib(files)
click to toggle source
# File lib/workspace/build.rb, line 89 def build_lib(files) @compiler.append_compiling_flag "-fPIC" if build_common(files) && link_shared(@target_short, Dir.glob("obj/*.o").reject { |f| f.start_with? 'obj/test_'}) puts "BUILDING SUCCEEDED".green else puts "building target FAILED".red end end
build_one_test(test_file, deps)
click to toggle source
# File lib/workspace/test.rb, line 98 def build_one_test(test_file, deps) compile_one_test(test_file, deps) link_one_test(test_file, deps) end
build_target()
click to toggle source
# File lib/workspace/build.rb, line 134 def build_target puts "#{'[BUILDING TARGET]'.magenta}..." target = "#{@target}/#{@name}" build_time = File.exist?(target) ? File.mtime(target) : Time.new(0) files = DepAnalyzer.compiling_filter target_deps, build_time, @source_suffix, @header_suffix if files.empty? && File.exist?(target) puts "nothing to do, all up to date" return true end self.send "build_#{@mode.to_s}", files end
build_test()
click to toggle source
# File lib/workspace/test.rb, line 128 def build_test puts "#{'[COMPILING TESTS]'.magenta}..." return unless test_build_time total_deps = fetch_all_deps compile_all_tests(total_deps) puts "#{'[100%]'.green} compiling done, starts linking..." puts "#{'[LINKING TESTS]'.magenta}..." # compilation and link are separated because they may be separated # by unexpected interrupt like C-c, C-d, etc. # thus unditionally link all tests link_all_tests(total_deps) puts "#{'[100%]'.green} linking done" end
clean_all()
click to toggle source
# File lib/workspace/clean.rb, line 10 def clean_all clean_target clean_obj end
clean_obj()
click to toggle source
# File lib/workspace/clean.rb, line 19 def clean_obj issue_command 'rm ./obj/* -rf' end
clean_target()
click to toggle source
# File lib/workspace/clean.rb, line 15 def clean_target issue_command 'rm ./target/* -rf' end
clean_tests()
click to toggle source
# File lib/workspace/clean.rb, line 23 def clean_tests issue_command 'rm ./obj/test_* -rf' issue_command 'rm ./target/test_* -rf' end
compile(f, o)
click to toggle source
# File lib/workspace/build.rb, line 64 def compile(f, o) @compiler.compile f, o end
compile_all_tests(deps)
click to toggle source
# File lib/workspace/test.rb, line 103 def compile_all_tests(deps) files = DepAnalyzer.compiling_filter(deps, test_build_time, @source_suffix, @header_suffix).select do |f| File.basename(f).start_with?('test_') end stepper = Stepper.new fetch_all_test_files.size, files.size files.each do |f| printf "#{stepper.progress_as_str.green} compiling #{f} " compile_one_test(f, deps) stepper.step end end
compile_one_test(test_file, deps)
click to toggle source
@deps is the dependency hash for tests cyclic dependency is not handled compiler should first be built
# File lib/workspace/test.rb, line 83 def compile_one_test(test_file, deps) extract_one_file(test_file, deps).each do |f| o = file_to_obj(f) next if File.exist?(o) && File.mtime(o) > File.mtime(f) && File.mtime(o) > File.mtime(hdr_of_src(f)) compile(f, o) end compile(test_file, file_to_obj(test_file)) end
create_working_files(prefix, filename)
click to toggle source
# File lib/workspace/add.rb, line 24 def create_working_files(prefix, filename) DefaultFiles.create_cpp filename, @source_suffix, @header_suffix DefaultFiles.create_hpp @name, prefix, filename, @header_suffix end
fetch_all_deps()
click to toggle source
# File lib/workspace/test.rb, line 69 def fetch_all_deps target_deps.merge(tests_deps) end
fetch_all_test_files()
click to toggle source
# File lib/workspace/test.rb, line 63 def fetch_all_test_files Dir.glob("#{@tests_short}/*.#{@source_suffix}").filter do |f| File.basename(f).start_with? 'test_' end end
get_deps(dep_file, source_dir, include_dirs)
click to toggle source
# File lib/workspace/build.rb, line 119 def get_deps(dep_file, source_dir, include_dirs) File.exist?(dep_file) ? DepAnalyzer.read_from(dep_file) : DepAnalyzer.new(source_dir, @source_suffix, @header_suffix).build_to_file(include_dirs, dep_file) end
link_all_tests(deps)
click to toggle source
# File lib/workspace/test.rb, line 117 def link_all_tests(deps) all_files = fetch_all_test_files stepper = Stepper.new all_files.size, all_files.size fetch_all_test_files.each do |f| printf "#{stepper.progress_as_str.green} linking #{File.basename(f, '.*').yellow}: " link_one_test(f, deps) stepper.step end end
link_exectutable(odir, objs)
click to toggle source
# File lib/workspace/build.rb, line 68 def link_exectutable(odir, objs) puts "#{"[100%]".green} linking" @compiler.link_executable "#{odir}/#{@name}", objs end
link_one_test(test_file, deps)
click to toggle source
# File lib/workspace/test.rb, line 93 def link_one_test(test_file, deps) target = "#{@target_short}/#{File.basename(test_file, '.*')}" @compiler.link_executable target, extract_one_file_obj(test_file, deps) + [file_to_obj(test_file)] end
target_deps()
click to toggle source
# File lib/workspace/build.rb, line 124 def target_deps get_deps @deps, @src_short, [@src_short, @components_short] end
test_all()
click to toggle source
# File lib/workspace/test.rb, line 33 def test_all build_test fetch_all_test_files.each do |f| test_single File.basename(f, '.*')['test_'.length..] end end
test_build_time()
click to toggle source
# File lib/workspace/test.rb, line 73 def test_build_time fetch_all_test_files.map do |f| obj = "#{@target_short}/#{File.basename(f, '.*')}" File.exist?(obj) ? File.mtime(obj) : Time.new(0) end.min end
test_single(name, args = "")
click to toggle source
# File lib/workspace/test.rb, line 40 def test_single(name, args = "") rebuild = false; bin = "#{@target_short}/test_#{name}" rebuild ||= !File.exist?(bin) file = "#{@tests_short}/test_#{name}.#{@source_suffix}" rebuild ||= File.mtime(bin) < File.mtime(file) deps = fetch_all_deps extract_one_file(file, deps).each do |f| rebuild ||= File.mtime(bin) < File.mtime(f) || File.mtime(bin) < File.mtime(hdr_of_src(f)) end cmd = "#{bin} #{args}" if rebuild build_compiler_from_config run_command cmd if build_one_test(file, deps) else run_command cmd end end
tests_deps()
click to toggle source
contain only headers sources in ./src/components are not included
# File lib/workspace/build.rb, line 130 def tests_deps get_deps @test_deps, @tests_short, [@src_short, @components_short] end