erebrus 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 76b069200add69a2d85d26e12f70dd726611b1873e77f9ce0f39b66b5c836efb
4
+ data.tar.gz: 407eb8f255a7bdc2cc495a66d5dc712e93f467ca8a01f20ff95e5762a89f1add
5
+ SHA512:
6
+ metadata.gz: e41e9a1e3878050ba948b036576a7630f34d94cbaa9c5e897ce07eadbac10e18fe79ce5aa0d882ff5336178fd7a504e2ec1728552a2dd3a85d7f4f3304831360
7
+ data.tar.gz: 4a35080e1c2bd7025cc66510457ceba4d9372254cf42916f4908cba345f8ccbdc97cc92a8d833819d577010a2ffdd3c58b515896531dce39b3c49b5c2443152d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daedalus OS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Erebrus
2
+
3
+ Modern C/C++ build system like msbuild, with ruby instead of xml
4
+
5
+ ## Usage
6
+
7
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
data/bin/erebrus ADDED
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env ruby
2
+ require "thor"
3
+ require "erebrus"
4
+
5
+ class ErebrusCommand < Thor
6
+ class_option :verbose, aliases: "-v", type: :boolean, desc: "Verbose output"
7
+ class_option :file, aliases: "-f", desc: "Buildfile to use", default: "Buildfile"
8
+
9
+ desc "build [TARGET]", "Builds the project with optional target"
10
+ option :namespace, aliases: "-n", desc: "Target namespace"
11
+ option :var, aliases: "-D", type: :hash, desc: "Set variables (e.g., -D CC=gcc -D CFLAGS=-O2)"
12
+ option :parallel, aliases: "-j", type: :numeric, desc: "Number of parallel jobs"
13
+ def build(target = nil)
14
+ load_buildfile_with_error_handling
15
+
16
+ target_name = resolve_target_name(target)
17
+
18
+ context = prepare_context
19
+
20
+ begin
21
+ Erebrus.build(target_name, context)
22
+ puts "Build completed successfully!" unless options[:quiet]
23
+ rescue Erebrus::Error => e
24
+ puts "Build failed: #{e.message}"
25
+ exit 1
26
+ rescue StandardError => e
27
+ puts "Unexpected error: #{e.message}"
28
+ puts e.backtrace if options[:verbose]
29
+ exit 1
30
+ end
31
+ end
32
+
33
+ desc "list [NAMESPACE]", "Lists all available targets, optionally filtered by namespace"
34
+ def list(namespace = nil)
35
+ load_buildfile_with_error_handling
36
+
37
+ begin
38
+ if namespace
39
+ Erebrus.list_targets(namespace: namespace)
40
+ else
41
+ Erebrus.list_targets
42
+ end
43
+ rescue StandardError => e
44
+ puts "Error listing targets: #{e.message}"
45
+ exit 1
46
+ end
47
+ end
48
+
49
+ desc "namespaces", "Lists all available namespaces"
50
+ def namespaces
51
+ load_buildfile_with_error_handling
52
+
53
+ begin
54
+ Erebrus.list_namespaces
55
+ rescue StandardError => e
56
+ puts "Error listing namespaces: #{e.message}"
57
+ exit 1
58
+ end
59
+ end
60
+
61
+ desc "init [TYPE]", "Initializes the project with a sample Buildfile"
62
+ def init(type = "basic")
63
+ buildfile = options[:file]
64
+
65
+ if File.exist?(buildfile)
66
+ puts "Buildfile '#{buildfile}' already exists!"
67
+ return
68
+ end
69
+
70
+ sample_content = case type.downcase
71
+ when "cpp", "c++"
72
+ generate_cpp_buildfile
73
+ when "c"
74
+ generate_c_buildfile
75
+ when "advanced"
76
+ generate_advanced_buildfile
77
+ else
78
+ generate_basic_buildfile
79
+ end
80
+
81
+ File.write(buildfile, sample_content)
82
+ puts "Created #{buildfile} (#{type} template)"
83
+ puts "Edit the file to define your build targets and run 'erebrus build' to build your project"
84
+ end
85
+
86
+ desc "validate", "Validates the Buildfile syntax"
87
+ def validate
88
+ buildfile = options[:file]
89
+
90
+ unless File.exist?(buildfile)
91
+ puts "Error: Buildfile '#{buildfile}' not found"
92
+ exit 1
93
+ end
94
+
95
+ begin
96
+ Erebrus.reset!
97
+ Erebrus.load_buildfile(buildfile)
98
+ puts "Buildfile is valid ✓"
99
+ rescue StandardError => e
100
+ puts "Buildfile validation failed: #{e.message}"
101
+ exit 1
102
+ end
103
+ end
104
+
105
+ desc "graph [TARGET]", "Shows dependency graph for target"
106
+ def graph(target = nil)
107
+ load_buildfile_with_error_handling
108
+
109
+ target_name = resolve_target_name(target)
110
+
111
+ begin
112
+ puts "Dependency graph for '#{target_name}':"
113
+ puts "(Graph visualization not yet implemented)"
114
+ rescue StandardError => e
115
+ puts "Error generating graph: #{e.message}"
116
+ exit 1
117
+ end
118
+ end
119
+
120
+ desc "clean", "Runs the clean target if available"
121
+ def clean
122
+ load_buildfile_with_error_handling
123
+
124
+ begin
125
+ Erebrus.build("clean")
126
+ rescue Erebrus::Error => e
127
+ if e.message.include?("not found")
128
+ puts "No clean target defined"
129
+ else
130
+ puts "Clean failed: #{e.message}"
131
+ exit 1
132
+ end
133
+ end
134
+ end
135
+
136
+ desc "version", "Show version"
137
+ def version
138
+ puts "Erebrus #{Erebrus::VERSION}"
139
+ end
140
+
141
+ private
142
+
143
+ def load_buildfile_with_error_handling
144
+ buildfile = options[:file]
145
+
146
+ unless File.exist?(buildfile)
147
+ puts "Error: Buildfile '#{buildfile}' not found"
148
+ puts "Run 'erebrus init' to create a sample Buildfile"
149
+ exit 1
150
+ end
151
+
152
+ begin
153
+ Erebrus.reset!
154
+ Erebrus.load_buildfile(buildfile)
155
+ rescue StandardError => e
156
+ puts "Error loading buildfile: #{e.message}"
157
+ puts e.backtrace if options[:verbose]
158
+ exit 1
159
+ end
160
+ end
161
+
162
+ def resolve_target_name(target)
163
+ return target unless target && options[:namespace]
164
+
165
+ "#{options[:namespace]}:#{target}"
166
+ end
167
+
168
+ def prepare_context
169
+ context = {}
170
+
171
+ context.merge!(options[:var]) if options[:var]
172
+
173
+ context["PLATFORM"] = RUBY_PLATFORM
174
+ context["RUBY_VERSION"] = RUBY_VERSION
175
+ context["PWD"] = Dir.pwd
176
+
177
+ context
178
+ end
179
+
180
+ def generate_basic_buildfile
181
+ <<~BUILDFILE
182
+ target :clean, description: "Clean build artifacts" do
183
+ remove "build"
184
+ remove "dist"
185
+ end
186
+
187
+ target :setup, description: "Setup build environment" do
188
+ mkdir "build"
189
+ mkdir "dist"
190
+ end
191
+
192
+ target :build, description: "Build the project" do
193
+ depends_on :setup
194
+ #{" "}
195
+ action do
196
+ puts "Building project..."
197
+ end
198
+ end
199
+
200
+ target :test, description: "Run tests" do
201
+ depends_on :build
202
+ #{" "}
203
+ action do
204
+ puts "Running tests..."
205
+ end
206
+ end
207
+
208
+ default :build
209
+ BUILDFILE
210
+ end
211
+
212
+ def generate_cpp_buildfile
213
+ <<~BUILDFILE
214
+ set_variable 'CXX', get_variable('CXX', 'g++')
215
+ set_variable 'CXXFLAGS', get_variable('CXXFLAGS', '-std=c++17 -Wall -Wextra -O2')
216
+ set_variable 'SRCDIR', 'src'
217
+ set_variable 'BUILDDIR', 'build'
218
+ set_variable 'BINDIR', 'bin'
219
+
220
+ target :clean, description: "Clean build artifacts" do
221
+ remove "\${BUILDDIR}"
222
+ remove "\${BINDIR}"
223
+ end
224
+
225
+ target :setup, description: "Setup build directories" do
226
+ mkdir "\${BUILDDIR}"
227
+ mkdir "\${BINDIR}"
228
+ end
229
+
230
+ target :compile, description: "Compile source files" do
231
+ depends_on :setup
232
+ depends_on_files "\${SRCDIR}/*.cpp", "\${SRCDIR}/*.hpp"
233
+ produces "\${BUILDDIR}/*.o"
234
+ #{" "}
235
+ run "\${CXX} \${CXXFLAGS} -c \${SRCDIR}/*.cpp"
236
+ run "mv *.o \${BUILDDIR}/ 2>/dev/null || true"
237
+ end
238
+
239
+ target :link, description: "Link executable" do
240
+ depends_on :compile
241
+ produces "\${BINDIR}/myapp"
242
+ #{" "}
243
+ run "\${CXX} \${BUILDDIR}/*.o -o \${BINDIR}/myapp"
244
+ end
245
+
246
+ target :build, description: "Build the complete project" do
247
+ depends_on :link
248
+ end
249
+
250
+ target :test, description: "Run tests" do
251
+ depends_on :build
252
+ #{" "}
253
+ run "\${BINDIR}/myapp --test"
254
+ end
255
+
256
+ target :install, description: "Install the application" do
257
+ depends_on :build
258
+ #{" "}
259
+ platform :linux do
260
+ copy "\${BINDIR}/myapp", "/usr/local/bin/myapp"
261
+ end
262
+ #{" "}
263
+ platform :windows do
264
+ copy "\${BINDIR}/myapp.exe", "C:/Program Files/MyApp/myapp.exe"
265
+ end
266
+ end
267
+
268
+ default :build
269
+ BUILDFILE
270
+ end
271
+
272
+ def generate_c_buildfile
273
+ <<~BUILDFILE
274
+ set_variable 'CC', get_variable('CC', 'gcc')
275
+ set_variable 'CFLAGS', get_variable('CFLAGS', '-std=c99 -Wall -Wextra -O2')
276
+
277
+ target :clean, description: "Clean build artifacts" do
278
+ remove "build"
279
+ remove "bin"
280
+ end
281
+
282
+ target :setup, description: "Setup build directories" do
283
+ mkdir "build"
284
+ mkdir "bin"
285
+ end
286
+
287
+ target :compile, description: "Compile source files" do
288
+ depends_on :setup
289
+ depends_on_files "src/*.c", "src/*.h"
290
+ #{" "}
291
+ run "\${CC} \${CFLAGS} -c src/*.c"
292
+ run "mv *.o build/ 2>/dev/null || true"
293
+ end
294
+
295
+ target :link, description: "Link executable" do
296
+ depends_on :compile
297
+ #{" "}
298
+ run "\${CC} build/*.o -o bin/myapp"
299
+ end
300
+
301
+ target :build, description: "Build the project" do
302
+ depends_on :link
303
+ end
304
+
305
+ default :build
306
+ BUILDFILE
307
+ end
308
+
309
+ def generate_advanced_buildfile
310
+ <<~BUILDFILE
311
+ set_variable 'PROJECT_NAME', 'MyProject'
312
+ set_variable 'VERSION', '1.0.0'
313
+ set_variable 'BUILD_TYPE', get_variable('BUILD_TYPE', 'Release')
314
+
315
+ conditional platform?(:windows) do
316
+ set_variable 'EXE_EXT', '.exe'
317
+ set_variable 'LIB_EXT', '.dll'
318
+ end
319
+
320
+ conditional platform?(:linux) do
321
+ set_variable 'EXE_EXT', ''
322
+ set_variable 'LIB_EXT', '.so'
323
+ end
324
+
325
+ namespace :clean do
326
+ target :all, description: "Clean everything" do
327
+ remove "build"
328
+ remove "dist"
329
+ remove "temp"
330
+ end
331
+
332
+ target :build, description: "Clean build artifacts only" do
333
+ remove "build"
334
+ end
335
+ end
336
+
337
+ namespace :build do
338
+ target :debug, description: "Build debug version" do
339
+ set_variable 'BUILD_TYPE', 'Debug'
340
+ depends_on 'compile:debug'
341
+ depends_on 'link:debug'
342
+ end
343
+
344
+ target :release, description: "Build release version" do
345
+ set_variable 'BUILD_TYPE', 'Release'
346
+ depends_on 'compile:release'
347
+ depends_on 'link:release'
348
+ end
349
+ end
350
+
351
+ namespace :compile do
352
+ target :debug, description: "Compile debug version" do
353
+ tag :compile, :debug
354
+ only_if ->(ctx) { ctx['BUILD_TYPE'] == 'Debug' }
355
+ #{" "}
356
+ run "g++ -g -DDEBUG -c src/*.cpp"
357
+ end
358
+
359
+ target :release, description: "Compile release version" do
360
+ tag :compile, :release
361
+ only_if ->(ctx) { ctx['BUILD_TYPE'] == 'Release' }
362
+ #{" "}
363
+ run "g++ -O2 -DNDEBUG -c src/*.cpp"
364
+ end
365
+ end
366
+
367
+ namespace :link do
368
+ target :debug, description: "Link debug executable" do
369
+ depends_on 'compile:debug'
370
+ run "g++ *.o -o bin/\${PROJECT_NAME}_debug\${EXE_EXT}"
371
+ end
372
+
373
+ target :release, description: "Link release executable" do
374
+ depends_on 'compile:release'
375
+ run "g++ *.o -o bin/\${PROJECT_NAME}\${EXE_EXT}"
376
+ end
377
+ end
378
+
379
+ namespace :test do
380
+ target :unit, description: "Run unit tests" do
381
+ depends_on 'build:debug'
382
+ run "bin/\${PROJECT_NAME}_debug\${EXE_EXT} --unit-tests"
383
+ end
384
+
385
+ target :integration, description: "Run integration tests" do
386
+ depends_on 'build:release'
387
+ run "bin/\${PROJECT_NAME}\${EXE_EXT} --integration-tests"
388
+ end
389
+
390
+ target :all, description: "Run all tests" do
391
+ depends_on :unit
392
+ depends_on :integration
393
+ end
394
+ end
395
+
396
+ default 'build:release'
397
+ BUILDFILE
398
+ end
399
+ end
400
+
401
+ ErebrusCommand.start
@@ -0,0 +1,96 @@
1
+ # Erebrus Buildfile Example
2
+ # This demonstrates the Ruby build DSL for C/C++ projects
3
+
4
+ target :clean, description: "Clean all build artifacts" do
5
+ remove "build"
6
+ remove "dist"
7
+ remove "obj"
8
+ end
9
+
10
+ target :setup, description: "Setup build directories" do
11
+ mkdir "build"
12
+ mkdir "dist"
13
+ mkdir "obj"
14
+ end
15
+
16
+ target :configure, description: "Configure build environment" do
17
+ depends_on :setup
18
+
19
+ action do |context|
20
+ puts "Configuring build for #{context[:platform] || 'default'} platform"
21
+ end
22
+ end
23
+
24
+ target :compile_sources, description: "Compile source files" do
25
+ depends_on :configure
26
+
27
+ # Compile all C++ files in src/
28
+ run "g++ -std=c++17 -Wall -Wextra -O2 -c src/*.cpp -Iinclude"
29
+
30
+ # Move object files to obj directory
31
+ run "mv *.o obj/ 2>/dev/null || true"
32
+ end
33
+
34
+ target :compile_tests, description: "Compile test files" do
35
+ depends_on :compile_sources
36
+
37
+ run "g++ -std=c++17 -Wall -Wextra -O2 -c tests/*.cpp -Iinclude -Isrc"
38
+ run "mv *.o obj/ 2>/dev/null || true"
39
+ end
40
+
41
+ target :link_main, description: "Link main executable" do
42
+ depends_on :compile_sources
43
+
44
+ run "g++ obj/*.o -o dist/myapp"
45
+ end
46
+
47
+ target :link_tests, description: "Link test executable" do
48
+ depends_on :compile_tests
49
+
50
+ run "g++ obj/*.o -o dist/test_runner"
51
+ end
52
+
53
+ target :build, description: "Build the main application" do
54
+ depends_on :link_main
55
+
56
+ action do
57
+ puts "Build completed successfully!"
58
+ puts "Executable: dist/myapp"
59
+ end
60
+ end
61
+
62
+ target :test, description: "Run all tests" do
63
+ depends_on :link_tests
64
+
65
+ run "dist/test_runner"
66
+ end
67
+
68
+ target :install, description: "Install the application" do
69
+ depends_on :build
70
+
71
+ copy "dist/myapp", "/usr/local/bin/myapp"
72
+
73
+ action do
74
+ puts "Application installed to /usr/local/bin/myapp"
75
+ end
76
+ end
77
+
78
+ target :package, description: "Create distribution package" do
79
+ depends_on :build
80
+
81
+ mkdir "package"
82
+ copy "dist/myapp", "package/"
83
+ copy "README.md", "package/"
84
+ copy "LICENSE", "package/"
85
+
86
+ run "tar -czf dist/myapp-1.0.tar.gz -C package ."
87
+ remove "package"
88
+ end
89
+
90
+ target :all, description: "Build, test, and package" do
91
+ depends_on :test
92
+ depends_on :package
93
+ end
94
+
95
+ # Set the default target
96
+ default :build
@@ -0,0 +1,449 @@
1
+ require "pathname"
2
+
3
+ module Erebrus
4
+ class BuildEngine
5
+ attr_reader :targets, :namespaces, :variables, :included_files
6
+ attr_accessor :current_namespace
7
+
8
+ def initialize
9
+ @targets = {}
10
+ @namespaces = {}
11
+ @variables = {}
12
+ @default_target = nil
13
+ @current_namespace = nil
14
+ @included_files = Set.new
15
+ @file_watchers = {}
16
+ @conditional_stack = []
17
+ end
18
+
19
+ def target(name, description: nil, namespace: nil, &block)
20
+ full_name = build_target_name(name, namespace || @current_namespace)
21
+ target_obj = Target.new(full_name, description: description)
22
+ target_obj.namespace = namespace || @current_namespace
23
+ @targets[full_name] = target_obj
24
+
25
+ ns = namespace || @current_namespace
26
+ if ns
27
+ @namespaces[ns] ||= []
28
+ @namespaces[ns] << full_name
29
+ end
30
+
31
+ if block_given?
32
+ target_context = TargetContext.new(target_obj, self)
33
+ target_context.instance_eval(&block)
34
+ end
35
+
36
+ target_obj
37
+ end
38
+
39
+ def namespace(name, &block)
40
+ old_namespace = @current_namespace
41
+ @current_namespace = name.to_s
42
+
43
+ begin
44
+ yield if block_given?
45
+ ensure
46
+ @current_namespace = old_namespace
47
+ end
48
+ end
49
+
50
+ def include_buildfile(file_path, namespace: nil)
51
+ resolved_path = resolve_file_path(file_path)
52
+
53
+ if @included_files.include?(resolved_path)
54
+ puts "Warning: Buildfile '#{resolved_path}' already included, skipping"
55
+ return
56
+ end
57
+
58
+ raise Error, "Buildfile not found: #{resolved_path}" unless File.exist?(resolved_path)
59
+
60
+ @included_files.add(resolved_path)
61
+
62
+ old_namespace = @current_namespace
63
+ @current_namespace = namespace if namespace
64
+
65
+ begin
66
+ content = File.read(resolved_path)
67
+ instance_eval(content, resolved_path)
68
+ ensure
69
+ @current_namespace = old_namespace
70
+ end
71
+ end
72
+
73
+ def set_variable(name, value)
74
+ @variables[name.to_s] = value
75
+ end
76
+
77
+ def get_variable(name, default = nil)
78
+ @variables[name.to_s] || ENV[name.to_s] || default
79
+ end
80
+
81
+ def conditional(condition, &block)
82
+ @conditional_stack.push(condition)
83
+
84
+ begin
85
+ yield if condition && block_given?
86
+ ensure
87
+ @conditional_stack.pop
88
+ end
89
+ end
90
+
91
+ def platform?(name)
92
+ case name.to_s.downcase
93
+ when "windows", "win32"
94
+ Gem.win_platform?
95
+ when "linux"
96
+ RUBY_PLATFORM.include?("linux")
97
+ when "macos", "darwin"
98
+ RUBY_PLATFORM.include?("darwin")
99
+ else
100
+ RUBY_PLATFORM.include?(name.to_s)
101
+ end
102
+ end
103
+
104
+ def file_exists?(path)
105
+ File.exist?(resolve_file_path(path))
106
+ end
107
+
108
+ def directory_exists?(path)
109
+ Dir.exist?(resolve_file_path(path))
110
+ end
111
+
112
+ def default(target_name)
113
+ @default_target = resolve_target_name(target_name)
114
+ end
115
+
116
+ def build(target_name = nil, context = {})
117
+ target_name = resolve_target_name(target_name || @default_target)
118
+
119
+ raise Error, "No target specified and no default target set" unless target_name
120
+
121
+ target_obj = @targets[target_name]
122
+ unless target_obj
123
+ available_targets = @targets.keys.sort
124
+ raise Error, "Target '#{target_name}' not found. Available targets: #{available_targets.join(", ")}"
125
+ end
126
+
127
+ merged_context = @variables.merge(context)
128
+
129
+ reset_all_targets!
130
+ execute_target(target_obj, merged_context)
131
+ end
132
+
133
+ def list_targets(namespace: nil)
134
+ targets_to_show = if namespace
135
+ @namespaces[namespace.to_s] || []
136
+ else
137
+ @targets.keys
138
+ end
139
+
140
+ if namespace
141
+ puts "Targets in namespace '#{namespace}':"
142
+ else
143
+ puts "Available targets:"
144
+ end
145
+
146
+ targets_to_show.sort.each do |name|
147
+ target = @targets[name]
148
+ next unless target
149
+
150
+ puts " #{name}:"
151
+ puts " Description: #{target.description}" if target.description
152
+ puts " Namespace: #{target.namespace}" if target.namespace && !namespace
153
+ puts " Dependencies: #{target.dependencies.join(", ")}" unless target.dependencies.empty?
154
+ puts " Conditions: #{target.conditions.join(", ")}" unless target.conditions.empty?
155
+ puts
156
+ end
157
+ end
158
+
159
+ def list_namespaces
160
+ puts "Available namespaces:"
161
+ @namespaces.each do |ns, targets|
162
+ puts " #{ns}: #{targets.size} targets"
163
+ end
164
+ end
165
+
166
+ def watch_file(pattern, &block)
167
+ @file_watchers[pattern] = block
168
+ end
169
+
170
+ def check_file_changes
171
+ @file_watchers.each do |pattern, callback|
172
+ Dir.glob(pattern).each do |file|
173
+ callback.call(file) if File.mtime(file) > (@last_check || Time.at(0))
174
+ end
175
+ end
176
+ @last_check = Time.now
177
+ end
178
+
179
+ private
180
+
181
+ def build_target_name(name, namespace = nil)
182
+ if namespace
183
+ "#{namespace}:#{name}"
184
+ else
185
+ name.to_s
186
+ end
187
+ end
188
+
189
+ def resolve_target_name(name)
190
+ return nil unless name
191
+
192
+ name_str = name.to_s
193
+
194
+ return name_str if name_str.include?(":")
195
+
196
+ if @current_namespace
197
+ namespaced = "#{@current_namespace}:#{name_str}"
198
+ return namespaced if @targets.key?(namespaced)
199
+ end
200
+
201
+ name_str
202
+ end
203
+
204
+ def resolve_file_path(path)
205
+ pathname = Pathname.new(path)
206
+ return pathname.to_s if pathname.absolute?
207
+
208
+ Pathname.pwd.join(path).to_s
209
+ end
210
+
211
+ def execute_target(target, context, visited = Set.new)
212
+ target_name = target.name
213
+
214
+ if visited.include?(target_name)
215
+ raise Error, "Circular dependency detected: #{visited.to_a.join(" -> ")} -> #{target_name}"
216
+ end
217
+
218
+ return if target.executed
219
+
220
+ unless target.conditions.all? { |condition| condition.call(context) }
221
+ puts "Skipping target '#{target_name}' (conditions not met)"
222
+ target.executed = true
223
+ return
224
+ end
225
+
226
+ visited.add(target_name)
227
+
228
+ target.dependencies.each do |dep_name|
229
+ resolved_dep = resolve_target_name(dep_name)
230
+ dep_target = @targets[resolved_dep]
231
+ raise Error, "Dependency '#{dep_name}' not found for target '#{target_name}'" unless dep_target
232
+
233
+ execute_target(dep_target, context, visited.dup)
234
+ end
235
+
236
+ target.execute(context)
237
+
238
+ visited.delete(target_name)
239
+ end
240
+
241
+ def reset_all_targets!
242
+ @targets.each_value(&:reset!)
243
+ end
244
+ end
245
+
246
+ class TargetContext
247
+ def initialize(target, engine = nil)
248
+ @target = target
249
+ @engine = engine
250
+ end
251
+
252
+ def depends_on(*deps)
253
+ @target.depends_on(*deps)
254
+ end
255
+
256
+ def depends_on_files(*files)
257
+ @target.depends_on_files(*files)
258
+ end
259
+
260
+ def produces(*files)
261
+ @target.produces(*files)
262
+ end
263
+
264
+ def condition(&block)
265
+ @target.condition(&block)
266
+ end
267
+
268
+ def only_if(condition)
269
+ @target.only_if(condition)
270
+ end
271
+
272
+ def tag(*tags)
273
+ @target.tag(*tags)
274
+ end
275
+
276
+ def action(&block)
277
+ @target.action(&block)
278
+ end
279
+
280
+ def run(command, options = {})
281
+ @target.action do |context|
282
+ expanded_command = expand_variables(command, context)
283
+ puts "Running: #{expanded_command}"
284
+
285
+ if options[:capture]
286
+ result = `#{expanded_command}`
287
+ raise Error, "Command failed: #{expanded_command}" unless $?.success?
288
+
289
+ result.chomp
290
+ else
291
+ system(expanded_command) || raise(Error, "Command failed: #{expanded_command}")
292
+ end
293
+ end
294
+ end
295
+
296
+ def copy(source, destination, options = {})
297
+ @target.action do |context|
298
+ expanded_source = expand_variables(source, context)
299
+ expanded_dest = expand_variables(destination, context)
300
+
301
+ puts "Copying #{expanded_source} to #{expanded_dest}"
302
+ require "fileutils"
303
+
304
+ if options[:preserve]
305
+ FileUtils.cp_r(expanded_source, expanded_dest, preserve: true)
306
+ else
307
+ FileUtils.cp_r(expanded_source, expanded_dest)
308
+ end
309
+ end
310
+ end
311
+
312
+ def mkdir(path)
313
+ @target.action do |context|
314
+ expanded_path = expand_variables(path, context)
315
+ puts "Creating directory: #{expanded_path}"
316
+ require "fileutils"
317
+ FileUtils.mkdir_p(expanded_path)
318
+ end
319
+ end
320
+
321
+ def remove(path)
322
+ @target.action do |context|
323
+ expanded_path = expand_variables(path, context)
324
+ puts "Removing: #{expanded_path}"
325
+ require "fileutils"
326
+ FileUtils.rm_rf(expanded_path)
327
+ end
328
+ end
329
+
330
+ def touch(path)
331
+ @target.action do |context|
332
+ expanded_path = expand_variables(path, context)
333
+ puts "Touching: #{expanded_path}"
334
+ require "fileutils"
335
+ FileUtils.touch(expanded_path)
336
+ end
337
+ end
338
+
339
+ def chmod(mode, path)
340
+ @target.action do |context|
341
+ expanded_path = expand_variables(path, context)
342
+ puts "Changing permissions of #{expanded_path} to #{mode}"
343
+ require "fileutils"
344
+ FileUtils.chmod(mode, expanded_path)
345
+ end
346
+ end
347
+
348
+ def download(url, destination)
349
+ @target.action do |context|
350
+ expanded_url = expand_variables(url, context)
351
+ expanded_dest = expand_variables(destination, context)
352
+
353
+ puts "Downloading #{expanded_url} to #{expanded_dest}"
354
+ require "net/http"
355
+ require "uri"
356
+
357
+ uri = URI(expanded_url)
358
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
359
+ request = Net::HTTP::Get.new(uri)
360
+ response = http.request(request)
361
+
362
+ raise Error, "Download failed: #{response.code} #{response.message}" unless response.code == "200"
363
+
364
+ File.write(expanded_dest, response.body)
365
+ end
366
+ end
367
+ end
368
+
369
+ def template(template_file, output_file, variables = {})
370
+ @target.action do |context|
371
+ expanded_template = expand_variables(template_file, context)
372
+ expanded_output = expand_variables(output_file, context)
373
+
374
+ puts "Processing template #{expanded_template} -> #{expanded_output}"
375
+
376
+ template_content = File.read(expanded_template)
377
+ merged_vars = context.merge(variables)
378
+
379
+ result = template_content.gsub(/\{\{(\w+)\}\}/) do |match|
380
+ var_name = ::Regexp.last_match(1)
381
+ merged_vars[var_name] || merged_vars[var_name.to_sym] || match
382
+ end
383
+
384
+ File.write(expanded_output, result)
385
+ end
386
+ end
387
+
388
+ def parallel(*targets)
389
+ @target.action do
390
+ threads = targets.map do |target_name|
391
+ Thread.new do
392
+ @engine&.build(target_name) if @engine
393
+ end
394
+ end
395
+ threads.each(&:join)
396
+ end
397
+ end
398
+
399
+ def conditional(condition, &block)
400
+ @target.action do |context|
401
+ result = case condition
402
+ when Proc
403
+ condition.call(context)
404
+ when Symbol
405
+ context[condition]
406
+ when String
407
+ context[condition] || ENV[condition]
408
+ else
409
+ !!condition
410
+ end
411
+
412
+ block.call(context) if result && block_given?
413
+ end
414
+ end
415
+
416
+ def platform(name, &block)
417
+ conditional(->(ctx) { @engine&.platform?(name) }, &block)
418
+ end
419
+
420
+ def file_exists(path, &block)
421
+ conditional(->(ctx) { File.exist?(expand_variables(path, ctx)) }, &block)
422
+ end
423
+
424
+ def directory_exists(path, &block)
425
+ conditional(->(ctx) { Dir.exist?(expand_variables(path, ctx)) }, &block)
426
+ end
427
+
428
+ def set_variable(name, value)
429
+ @target.action do |context|
430
+ @engine&.set_variable(name, expand_variables(value, context))
431
+ end
432
+ end
433
+
434
+ def get_variable(name, default = nil)
435
+ @engine&.get_variable(name, default)
436
+ end
437
+
438
+ private
439
+
440
+ def expand_variables(text, context)
441
+ return text unless text.is_a?(String)
442
+
443
+ text.gsub(/\$\{(\w+)\}|\$(\w+)/) do |match|
444
+ var_name = ::Regexp.last_match(1) || ::Regexp.last_match(2)
445
+ context[var_name] || context[var_name.to_sym] || ENV[var_name] || match
446
+ end
447
+ end
448
+ end
449
+ end
@@ -0,0 +1,165 @@
1
+ module Erebrus
2
+ module DSL
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def build_engine
9
+ @build_engine ||= BuildEngine.new
10
+ end
11
+
12
+ def target(name, description: nil, namespace: nil, &block)
13
+ build_engine.target(name, description: description, namespace: namespace, &block)
14
+ end
15
+
16
+ def namespace(name, &block)
17
+ build_engine.namespace(name, &block)
18
+ end
19
+
20
+ def include_buildfile(file_path, namespace: nil)
21
+ build_engine.include_buildfile(file_path, namespace: namespace)
22
+ end
23
+
24
+ def set_variable(name, value)
25
+ build_engine.set_variable(name, value)
26
+ end
27
+
28
+ def get_variable(name, default = nil)
29
+ build_engine.get_variable(name, default)
30
+ end
31
+
32
+ def conditional(condition, &block)
33
+ build_engine.conditional(condition, &block)
34
+ end
35
+
36
+ def platform?(name)
37
+ build_engine.platform?(name)
38
+ end
39
+
40
+ def default(target_name)
41
+ build_engine.default(target_name)
42
+ end
43
+
44
+ def build(target_name = nil, context = {})
45
+ build_engine.build(target_name, context)
46
+ end
47
+
48
+ def list_targets(namespace: nil)
49
+ build_engine.list_targets(namespace: namespace)
50
+ end
51
+
52
+ def list_namespaces
53
+ build_engine.list_namespaces
54
+ end
55
+ end
56
+
57
+ def target(name, description: nil, namespace: nil, &block)
58
+ self.class.build_engine.target(name, description: description, namespace: namespace, &block)
59
+ end
60
+
61
+ def namespace(name, &block)
62
+ self.class.build_engine.namespace(name, &block)
63
+ end
64
+
65
+ def include_buildfile(file_path, namespace: nil)
66
+ self.class.build_engine.include_buildfile(file_path, namespace: namespace)
67
+ end
68
+
69
+ def set_variable(name, value)
70
+ self.class.build_engine.set_variable(name, value)
71
+ end
72
+
73
+ def get_variable(name, default = nil)
74
+ self.class.build_engine.get_variable(name, default)
75
+ end
76
+
77
+ def conditional(condition, &block)
78
+ self.class.build_engine.conditional(condition, &block)
79
+ end
80
+
81
+ def platform?(name)
82
+ self.class.build_engine.platform?(name)
83
+ end
84
+
85
+ def default(target_name)
86
+ self.class.build_engine.default(target_name)
87
+ end
88
+
89
+ def build(target_name = nil, context = {})
90
+ self.class.build_engine.build(target_name, context)
91
+ end
92
+
93
+ def list_targets(namespace: nil)
94
+ self.class.build_engine.list_targets(namespace: namespace)
95
+ end
96
+
97
+ def list_namespaces
98
+ self.class.build_engine.list_namespaces
99
+ end
100
+ end
101
+
102
+ def self.target(name, description: nil, namespace: nil, &block)
103
+ @global_engine ||= BuildEngine.new
104
+ @global_engine.target(name, description: description, namespace: namespace, &block)
105
+ end
106
+
107
+ def self.namespace(name, &block)
108
+ @global_engine ||= BuildEngine.new
109
+ @global_engine.namespace(name, &block)
110
+ end
111
+
112
+ def self.include_buildfile(file_path, namespace: nil)
113
+ @global_engine ||= BuildEngine.new
114
+ @global_engine.include_buildfile(file_path, namespace: namespace)
115
+ end
116
+
117
+ def self.set_variable(name, value)
118
+ @global_engine ||= BuildEngine.new
119
+ @global_engine.set_variable(name, value)
120
+ end
121
+
122
+ def self.get_variable(name, default = nil)
123
+ @global_engine ||= BuildEngine.new
124
+ @global_engine.get_variable(name, default)
125
+ end
126
+
127
+ def self.conditional(condition, &block)
128
+ @global_engine ||= BuildEngine.new
129
+ @global_engine.conditional(condition, &block)
130
+ end
131
+
132
+ def self.platform?(name)
133
+ @global_engine ||= BuildEngine.new
134
+ @global_engine.platform?(name)
135
+ end
136
+
137
+ def self.default(target_name)
138
+ @global_engine ||= BuildEngine.new
139
+ @global_engine.default(target_name)
140
+ end
141
+
142
+ def self.build(target_name = nil, context = {})
143
+ @global_engine ||= BuildEngine.new
144
+ @global_engine.build(target_name, context)
145
+ end
146
+
147
+ def self.list_targets(namespace: nil)
148
+ @global_engine ||= BuildEngine.new
149
+ @global_engine.list_targets(namespace: namespace)
150
+ end
151
+
152
+ def self.list_namespaces
153
+ @global_engine ||= BuildEngine.new
154
+ @global_engine.list_namespaces
155
+ end
156
+
157
+ def self.load_buildfile(file_path)
158
+ @global_engine ||= BuildEngine.new
159
+ @global_engine.include_buildfile(file_path)
160
+ end
161
+
162
+ def self.reset!
163
+ @global_engine = nil
164
+ end
165
+ end
@@ -0,0 +1,130 @@
1
+ module Erebrus
2
+ class Target
3
+ attr_reader :name, :dependencies, :description, :conditions, :file_dependencies, :outputs
4
+ attr_accessor :executed, :namespace, :tags, :priority
5
+
6
+ def initialize(name, description: nil)
7
+ @name = name.to_s
8
+ @description = description
9
+ @dependencies = []
10
+ @actions = []
11
+ @conditions = []
12
+ @file_dependencies = []
13
+ @outputs = []
14
+ @executed = false
15
+ @namespace = nil
16
+ @tags = []
17
+ @priority = 0
18
+ @last_run = nil
19
+ end
20
+
21
+ def depends_on(*deps)
22
+ @dependencies.concat(deps.map(&:to_s))
23
+ self
24
+ end
25
+
26
+ def depends_on_files(*files)
27
+ @file_dependencies.concat(files.map(&:to_s))
28
+ self
29
+ end
30
+
31
+ def produces(*files)
32
+ @outputs.concat(files.map(&:to_s))
33
+ self
34
+ end
35
+
36
+ def condition(&block)
37
+ @conditions << block if block_given?
38
+ self
39
+ end
40
+
41
+ def only_if(condition)
42
+ @conditions << case condition
43
+ when Proc
44
+ condition
45
+ when Symbol
46
+ ->(ctx) { ctx[condition] }
47
+ when String
48
+ ->(ctx) { ctx[condition] || ENV[condition] }
49
+ else
50
+ ->(_) { !!condition }
51
+ end
52
+ self
53
+ end
54
+
55
+ def tag(*tags)
56
+ @tags.concat(tags.map(&:to_s))
57
+ self
58
+ end
59
+
60
+ def action(&block)
61
+ @actions << block if block_given?
62
+ self
63
+ end
64
+
65
+ def needs_execution?(context = {})
66
+ return true if @executed == false
67
+
68
+ return true if file_dependencies_changed?
69
+
70
+ @conditions.any? { |condition| !condition.call(context) }
71
+ end
72
+
73
+ def execute(context = {})
74
+ return if @executed
75
+
76
+ start_time = Time.now
77
+
78
+ puts "Executing target: #{@name}"
79
+ puts " #{@description}" if @description
80
+ puts " Namespace: #{@namespace}" if @namespace
81
+ puts " Tags: #{@tags.join(", ")}" unless @tags.empty?
82
+
83
+ begin
84
+ @actions.each_with_index do |action, index|
85
+ puts " Action #{index + 1}/#{@actions.size}" if @actions.size > 1
86
+
87
+ if action.arity == 0
88
+ action.call
89
+ else
90
+ action.call(context)
91
+ end
92
+ end
93
+
94
+ @executed = true
95
+ @last_run = Time.now
96
+
97
+ execution_time = Time.now - start_time
98
+ puts " Completed in #{execution_time.round(2)}s"
99
+ rescue StandardError => e
100
+ puts " Failed: #{e.message}"
101
+ raise Error, "Target '#{@name}' failed: #{e.message}"
102
+ end
103
+ end
104
+
105
+ def reset!
106
+ @executed = false
107
+ end
108
+
109
+ def file_dependencies_changed?
110
+ return false if @file_dependencies.empty? || @outputs.empty?
111
+
112
+ output_times = @outputs.map do |file|
113
+ File.exist?(file) ? File.mtime(file) : Time.at(0)
114
+ end
115
+ oldest_output = output_times.min
116
+
117
+ @file_dependencies.any? do |file|
118
+ File.exist?(file) && File.mtime(file) > oldest_output
119
+ end
120
+ end
121
+
122
+ def to_s
123
+ @name
124
+ end
125
+
126
+ def inspect
127
+ "#<Erebrus::Target:#{@name} ns=#{@namespace} deps=#{@dependencies} executed=#{@executed}>"
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module Erebrus
2
+ VERSION = "0.1.0"
3
+ end
data/lib/erebrus.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative "erebrus/version"
2
+ require_relative "erebrus/target"
3
+ require_relative "erebrus/build_engine"
4
+ require_relative "erebrus/dsl"
5
+
6
+ module Erebrus
7
+ class Error < StandardError; end
8
+
9
+ extend DSL::ClassMethods
10
+ end
data/sig/erebrus.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Erebrus
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: erebrus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jel9
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.4'
26
+ email:
27
+ - chloedev@proton.me
28
+ executables:
29
+ - erebrus
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - LICENSE
34
+ - README.md
35
+ - Rakefile
36
+ - bin/erebrus
37
+ - examples/Buildfile
38
+ - lib/erebrus.rb
39
+ - lib/erebrus/build_engine.rb
40
+ - lib/erebrus/dsl.rb
41
+ - lib/erebrus/target.rb
42
+ - lib/erebrus/version.rb
43
+ - sig/erebrus.rbs
44
+ homepage: https://github.com/daedalus-os/erebrus
45
+ licenses: []
46
+ metadata:
47
+ homepage_uri: https://github.com/daedalus-os/erebrus
48
+ source_code_uri: https://github.com/daedalus-os/erebrus
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.2.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: Modern C/C++ build system like msbuild, with ruby instead of xml
66
+ test_files: []