comet-build 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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