comet-build 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +59 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +14 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +58 -0
  7. data/LICENSE +9 -0
  8. data/README.md +311 -0
  9. data/Rakefile +1 -0
  10. data/bin/comet +30 -0
  11. data/comet-build.gemspec +26 -0
  12. data/lib/comet/dsl/bin_output.rb +19 -0
  13. data/lib/comet/dsl/c/source.rb +57 -0
  14. data/lib/comet/dsl/cpp/source.rb +57 -0
  15. data/lib/comet/dsl/elf_output.rb +19 -0
  16. data/lib/comet/dsl/firmware.rb +65 -0
  17. data/lib/comet/dsl/hardware.rb +56 -0
  18. data/lib/comet/dsl/helpers.rb +9 -0
  19. data/lib/comet/dsl/hex_output.rb +19 -0
  20. data/lib/comet/dsl/include_walker.rb +42 -0
  21. data/lib/comet/dsl/library.rb +21 -0
  22. data/lib/comet/dsl/linker.rb +45 -0
  23. data/lib/comet/dsl/map_output.rb +19 -0
  24. data/lib/comet/dsl/native/source.rb +44 -0
  25. data/lib/comet/dsl/options.rb +44 -0
  26. data/lib/comet/dsl/software.rb +42 -0
  27. data/lib/comet/dsl/source.rb +24 -0
  28. data/lib/comet/dsl/target.rb +64 -0
  29. data/lib/comet/makefile.rb +129 -0
  30. data/lib/comet/parser.rb +114 -0
  31. data/lib/comet/rules/alias.rb +29 -0
  32. data/lib/comet/rules/build.rb +118 -0
  33. data/lib/comet/rules/clean.rb +29 -0
  34. data/lib/comet/rules/codegen.rb +51 -0
  35. data/lib/comet/rules/compile.rb +72 -0
  36. data/lib/comet/rules/error.rb +33 -0
  37. data/lib/comet/rules/link.rb +74 -0
  38. data/lib/comet/rules/merge.rb +37 -0
  39. data/lib/comet/rules/objcopy.rb +41 -0
  40. data/lib/comet/rules/tmpdir.rb +39 -0
  41. data/lib/comet/version.rb +3 -0
  42. data/lib/comet.rb +76 -0
  43. data/spec/comet/complex_c_program/Makefile +57 -0
  44. data/spec/comet/complex_c_program/comet.rb +43 -0
  45. data/spec/comet/complex_c_program/include/public_api.h +2 -0
  46. data/spec/comet/complex_c_program/src/common.h +3 -0
  47. data/spec/comet/complex_c_program/src/library.h +2 -0
  48. data/spec/comet/complex_c_program/src/main.c +6 -0
  49. data/spec/comet/complex_c_program/src/module_a/impl.c +1 -0
  50. data/spec/comet/complex_c_program/src/module_a/impl.h +1 -0
  51. data/spec/comet/complex_c_program/src/module_b/impl.c +1 -0
  52. data/spec/comet/complex_c_program/src/module_b/impl.h +1 -0
  53. data/spec/comet/empty_build_file/Makefile +10 -0
  54. data/spec/comet/empty_build_file/comet.rb +0 -0
  55. data/spec/comet/empty_build_file/folder/.keep +0 -0
  56. data/spec/comet/generator_spec.rb +67 -0
  57. data/spec/comet/nested_circular_reference/Makefile +5 -0
  58. data/spec/comet/nested_circular_reference/comet.rb +8 -0
  59. data/spec/comet/no_build_file/Makefile +5 -0
  60. data/spec/comet/self_referential/Makefile +5 -0
  61. data/spec/comet/self_referential/comet.rb +5 -0
  62. data/spec/comet/simple_c_program/Makefile +30 -0
  63. data/spec/comet/simple_c_program/comet.rb +15 -0
  64. data/spec/comet/simple_c_program/main.c +5 -0
  65. data/spec/comet/simple_c_program/stuff.h +0 -0
  66. metadata +215 -0
@@ -0,0 +1,65 @@
1
+ module Comet
2
+ module DSL
3
+ class Firmware
4
+ include Comet::DSL::Helpers
5
+ alias inject instance_exec
6
+
7
+ def target(device, &block)
8
+ raise "target #{device} redefined" if @targets.key? device
9
+ @targets[device] = Target.new(device, &block)
10
+ end
11
+
12
+ def initialize(name, imports:, &block)
13
+ @name = name
14
+ @imports = imports
15
+ @targets = {}
16
+
17
+ instance_exec(&block) if block_given?
18
+ end
19
+
20
+ def to_s
21
+ "firmware #{@name}"
22
+ end
23
+
24
+ attr_reader :name
25
+ attr_accessor :imports
26
+
27
+ def targets
28
+ @targets.keys
29
+ end
30
+
31
+ def target_for(device)
32
+ @targets[device]
33
+ end
34
+
35
+ def hardware_for(device)
36
+ @imports.flat_map do |imported|
37
+ imported.hardware_for device
38
+ end.uniq
39
+ end
40
+
41
+ def validate!
42
+ raise "firmware `#{@name}' has no targets" if @targets.empty?
43
+
44
+ @targets.values.each(&:validate!)
45
+ imports.each(&:validate!)
46
+
47
+ targets.each do |device|
48
+ validate_for! device, hardware_for(device)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def validate_for!(device, hardware)
55
+ if hardware.empty?
56
+ raise "firmware `#{@name}' does not support target `#{device}'"
57
+ end
58
+
59
+ linkers = hardware.select(&:linker?)
60
+ raise "no linker section for firmware `#{@name}'" if linkers.empty?
61
+ raise "multiple linkers for firmware `#{@name}'" if linkers.length > 1
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ module Comet
2
+ module DSL
3
+ class Hardware
4
+ include Comet::DSL::Helpers
5
+ alias inject instance_exec
6
+
7
+ def source(language:, **kwargs, &block)
8
+ @sources.push Source.create(language: language, **kwargs, &block)
9
+ end
10
+
11
+ def linker(*args, **kwargs, &block)
12
+ raise 'linker section redefined' unless @linker_.nil?
13
+ @linker_ = Linker.new(*args, **kwargs, &block)
14
+ end
15
+
16
+ def import(name, path: nil)
17
+ @libraries.add Library.new(name, path: path)
18
+ end
19
+
20
+ def initialize(name, targets:, &block)
21
+ @name = name
22
+ @targets = targets
23
+ @sources = []
24
+ @linker_ = nil
25
+ @libraries = Set[]
26
+
27
+ instance_exec(&block) if block_given?
28
+ end
29
+
30
+ def to_s
31
+ "hardware #{@name} for #{@targets}"
32
+ end
33
+
34
+ attr_reader :targets
35
+ attr_reader :sources
36
+ attr_reader :linker_
37
+
38
+ def libraries
39
+ @libraries.to_a
40
+ end
41
+
42
+ def hardware_for(device)
43
+ device == targets ? self : []
44
+ end
45
+
46
+ def linker?
47
+ !@linker_.nil?
48
+ end
49
+
50
+ def validate!
51
+ sources.each(&:validate!)
52
+ @linker.validate! unless @linker.nil?
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,9 @@
1
+ module Comet
2
+ module DSL
3
+ module Helpers
4
+ def quote(string)
5
+ "\\\"#{string.strip}\\\""
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module Comet
2
+ module DSL
3
+ class HexOutput
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def to_s
9
+ 'hex output'
10
+ end
11
+
12
+ attr_reader :path
13
+
14
+ def validate!
15
+ raise "#{self} path is invalid" if @path.strip.empty?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ module Comet
2
+ module DSL
3
+ module IncludeWalker
4
+ class << self
5
+ def includes_for(file, include_paths)
6
+ @@cache[[file, include_paths]]
7
+ end
8
+
9
+ def search(file, include_paths)
10
+ includes = []
11
+
12
+ File.read(file).scan(PATTERN) do
13
+ header = Regexp.last_match[1] || Regexp.last_match[2]
14
+ includes << find_included_file(header, include_paths)
15
+ end
16
+
17
+ includes.compact.uniq.dup.each do |included|
18
+ includes.concat search(included, include_paths)
19
+ end
20
+
21
+ includes.compact.uniq
22
+ end
23
+
24
+ private
25
+
26
+ @@cache = Hash.new do |h, k|
27
+ h[k] = IncludeWalker.search(*k)
28
+ end
29
+
30
+ PATTERN = /^\s*#\s*include\s*(?:(?:<([^>]+)>)|(?:"([^"]+)"))\s*$/m
31
+
32
+ def find_included_file(file, include_paths)
33
+ match = include_paths.detect do |include_path|
34
+ File.exist? File.join(include_path, file)
35
+ end
36
+
37
+ File.join(match, file) unless match.nil?
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ module Comet
2
+ module DSL
3
+ class Library
4
+ def initialize(name, path: nil)
5
+ @name = name
6
+ @path = path
7
+ end
8
+
9
+ def to_s
10
+ "library #{@name}"
11
+ end
12
+
13
+ attr_reader :name
14
+ attr_reader :path
15
+
16
+ def validate!
17
+ true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module Comet
2
+ module DSL
3
+ class Linker
4
+ include Comet::DSL::Helpers
5
+ alias inject instance_exec
6
+
7
+ def script(script)
8
+ @script = script
9
+ end
10
+
11
+ def option(*args, **kwargs)
12
+ @options.add('', *args, **kwargs)
13
+ end
14
+
15
+ def initialize(triple, isa:, cpu:, opt:, &block)
16
+ @triple = triple
17
+ @isa = isa
18
+ @cpu = cpu
19
+ @options = Options.new
20
+ @opt = opt
21
+ @script = nil
22
+
23
+ instance_exec(&block) if block_given?
24
+ end
25
+
26
+ def to_s
27
+ "linker #{@triple}"
28
+ end
29
+
30
+ attr_reader :triple
31
+ attr_reader :isa
32
+ attr_reader :cpu
33
+ attr_reader :options
34
+ attr_reader :opt
35
+
36
+ def script_
37
+ @script
38
+ end
39
+
40
+ def validate!
41
+ raise "#{self} triple is invalid" if @triple.strip.empty?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,19 @@
1
+ module Comet
2
+ module DSL
3
+ class MapOutput
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def to_s
9
+ 'map output'
10
+ end
11
+
12
+ attr_reader :path
13
+
14
+ def validate!
15
+ raise "#{self} path is invalid" if @path.strip.empty?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module Comet
2
+ module DSL
3
+ module Native
4
+ class Source
5
+ include Comet::DSL::Helpers
6
+ alias inject instance_exec
7
+
8
+ def import(pattern)
9
+ @imports.push pattern
10
+ end
11
+
12
+ def initialize(**_, &block)
13
+ @imports = []
14
+
15
+ instance_exec(&block) if block_given?
16
+ end
17
+
18
+ def to_s
19
+ 'native source'
20
+ end
21
+
22
+ attr_reader :imports
23
+
24
+ def language
25
+ :native
26
+ end
27
+
28
+ def native?
29
+ true
30
+ end
31
+
32
+ def files
33
+ @imports.flat_map do |pattern|
34
+ Dir.glob pattern
35
+ end.uniq
36
+ end
37
+
38
+ def validate!
39
+ raise "#{self} references no source files" if @imports.empty?
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ module Comet
2
+ module DSL
3
+ class Options
4
+ def initialize
5
+ @options = {}
6
+ end
7
+
8
+ def to_s
9
+ 'options'
10
+ end
11
+
12
+ def add(prefix, *args, **kwargs)
13
+ args.each do |key|
14
+ set_option "#{prefix}#{key}"
15
+ end
16
+
17
+ kwargs.each do |key, value|
18
+ set_option "#{prefix}#{key}", value
19
+ end
20
+ end
21
+
22
+ def format(prefix: '-', separator: '=')
23
+ @options.map do |key, value|
24
+ next "#{prefix}#{key}" if value.nil?
25
+ "#{prefix}#{key}#{separator}#{value}"
26
+ end
27
+ end
28
+
29
+ def validate!
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ def set_option(key, value = nil)
36
+ if value == :remove
37
+ @options.delete key
38
+ else
39
+ @options[key] = value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ module Comet
2
+ module DSL
3
+ class Software
4
+ include Comet::DSL::Helpers
5
+ alias inject instance_exec
6
+
7
+ def source(language:, **kwargs, &block)
8
+ source = Source.create(language: language, **kwargs, &block)
9
+ raise 'native source only allowed in hardware block' if source.native?
10
+ @sources.push source
11
+ end
12
+
13
+ def initialize(name, depends:, &block)
14
+ @name = name
15
+ @depends = depends
16
+ @sources = []
17
+
18
+ instance_exec(&block) if block_given?
19
+ end
20
+
21
+ def to_s
22
+ "software #{@name}"
23
+ end
24
+
25
+ attr_accessor :depends
26
+ attr_reader :sources
27
+
28
+ def hardware_for(device)
29
+ @depends.flat_map do |dependent|
30
+ dependent.hardware_for device
31
+ end.uniq
32
+ end
33
+
34
+ def validate!
35
+ raise "#{@name} has no source directives" if sources.empty?
36
+
37
+ depends.each(&:validate!)
38
+ sources.each(&:validate!)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ module Comet
2
+ module DSL
3
+ class Source
4
+ class << self
5
+ def create(language:, **kwargs, &block)
6
+ raise "unknown language #{language}" unless valid? language
7
+ LANGUAGES[language].new(**kwargs, &block) # pass extra args
8
+ end
9
+
10
+ private
11
+
12
+ LANGUAGES = {
13
+ c: C::Source,
14
+ cpp: CPP::Source,
15
+ native: Native::Source
16
+ }.freeze
17
+
18
+ def valid?(language)
19
+ LANGUAGES.keys.include? language
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,64 @@
1
+ module Comet
2
+ module DSL
3
+ class Target
4
+ include Comet::DSL::Helpers
5
+ alias inject instance_exec
6
+
7
+ def elf(path)
8
+ @elf_output = ElfOutput.new(path)
9
+ end
10
+
11
+ def bin(path)
12
+ @bin_output = BinOutput.new(path)
13
+ end
14
+
15
+ def hex(path)
16
+ @hex_output = HexOutput.new(path)
17
+ end
18
+
19
+ def map(path)
20
+ @map_output = MapOutput.new(path)
21
+ end
22
+
23
+ def initialize(device, &block)
24
+ @elf_output = nil
25
+ @bin_output = nil
26
+ @hex_output = nil
27
+ @map_output = nil
28
+ @device = device
29
+
30
+ instance_exec(&block) if block_given?
31
+ end
32
+
33
+ def to_s
34
+ "target for #{@device}"
35
+ end
36
+
37
+ attr_reader :elf_output
38
+ attr_reader :bin_output
39
+ attr_reader :hex_output
40
+ attr_reader :map_output
41
+
42
+ def elf?
43
+ !@elf_output.nil?
44
+ end
45
+
46
+ def bin?
47
+ !@bin_output.nil?
48
+ end
49
+
50
+ def hex?
51
+ !@hex_output.nil?
52
+ end
53
+
54
+ def map?
55
+ !@map_output.nil?
56
+ end
57
+
58
+ def validate!
59
+ has_outputs = [elf?, bin?, hex?, map?].any?
60
+ raise "#{self} describes no outputs" unless has_outputs
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,129 @@
1
+ require 'digest'
2
+ require 'set'
3
+
4
+ module Comet
5
+ class Makefile
6
+ def initialize
7
+ @rules = [tmpdir_rule]
8
+ end
9
+
10
+ def contents
11
+ (commands + @rules.map(&:contents)).flatten.join "\n"
12
+ end
13
+
14
+ def execute(*args)
15
+ process_spawn make_command(*args) do |stdin|
16
+ stdin.write contents
17
+ end
18
+ end
19
+
20
+ def alias(name, rules)
21
+ insert Rules::Alias.new(name, rules)
22
+ end
23
+
24
+ def build(firmware, device)
25
+ append Rules::Build.new(firmware, device)
26
+ end
27
+
28
+ def error(message)
29
+ insert Rules::Error.new(message)
30
+ end
31
+
32
+ def clean
33
+ append Rules::Clean.new(Comet::TMPDIR)
34
+ end
35
+
36
+ private
37
+
38
+ def tmpdir_rule
39
+ @tmpdir_rule ||= Rules::TmpDir.new
40
+ end
41
+
42
+ def insert(rule)
43
+ @rules.unshift rule unless seen? rule
44
+ rule.rules.each(&method(:append))
45
+ rule
46
+ end
47
+
48
+ def append(rule)
49
+ @rules.push rule unless seen? rule
50
+ rule.rules.each(&method(:append))
51
+ rule
52
+ end
53
+
54
+ def seen?(rule)
55
+ @rules.map(&:target).include? rule.target
56
+ end
57
+
58
+ def process_spawn(arguments)
59
+ IO.pipe do |read, write|
60
+ process = spawn(*arguments, in: read)
61
+ yield write # pass in process stdin
62
+ write.close # make sure to close
63
+ Process.wait process
64
+ end
65
+
66
+ $?.exitstatus
67
+ end
68
+
69
+ def commands
70
+ commands = @rules.map(&:commands).reduce(&:merge)
71
+ commands.flat_map do |name, default|
72
+ ["#{name} ?= #{default}"]
73
+ end
74
+ end
75
+
76
+ def make_command(arguments, verbose: false)
77
+ [
78
+ ENV.fetch('MAKE', 'make'),
79
+ verbose ? nil : '-s',
80
+ '-f', '-', *arguments
81
+ ].compact
82
+ end
83
+
84
+ # Make utilities (TODO: move this elsewhere?)
85
+
86
+ class << self
87
+ def select_command(preferred, alternate, args)
88
+ return command(alternate, args) if preferred == alternate
89
+
90
+ check_command = "(command -v #{preferred} > /dev/null)"
91
+ preferred_cmd = command preferred, args
92
+ alternate_cmd = command alternate, args
93
+
94
+ "#{check_command} && #{preferred_cmd} || #{alternate_cmd}"
95
+ end
96
+
97
+ def command(command, args)
98
+ [command, *args].join ' '
99
+ end
100
+
101
+ def fingerprint(dependencies, extension:)
102
+ File.join Comet::TMPDIR, "#{hash dependencies}.#{extension}"
103
+ end
104
+
105
+ private
106
+
107
+ def hash(object)
108
+ digest = Digest::SHA256.new
109
+
110
+ if [Array, Set, Hash].any? { |t| object.is_a? t }
111
+ hash_container digest, container: object
112
+ else
113
+ digest.update Digest::SHA256.digest(object.class.name)
114
+ digest.update Digest::SHA256.digest(object.to_s)
115
+ end
116
+
117
+ digest.hexdigest[0..19]
118
+ end
119
+
120
+ def hash_container(digest, container:)
121
+ if container.is_a? Array
122
+ container.each { |element| digest.update hash(element) }
123
+ else # this will work for both hashes and sets
124
+ digest.update hash(container.to_a.sort)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end