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