erebrus 0.1.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76b069200add69a2d85d26e12f70dd726611b1873e77f9ce0f39b66b5c836efb
4
- data.tar.gz: 407eb8f255a7bdc2cc495a66d5dc712e93f467ca8a01f20ff95e5762a89f1add
3
+ metadata.gz: c0a0cf035d47b272eefe1385dd63919f5b6c58c75765248dc165f3ba942057b6
4
+ data.tar.gz: 637e125435680c16aefe62e318bc78990b2b0e1910d3c669071757996442279c
5
5
  SHA512:
6
- metadata.gz: e41e9a1e3878050ba948b036576a7630f34d94cbaa9c5e897ce07eadbac10e18fe79ce5aa0d882ff5336178fd7a504e2ec1728552a2dd3a85d7f4f3304831360
7
- data.tar.gz: 4a35080e1c2bd7025cc66510457ceba4d9372254cf42916f4908cba345f8ccbdc97cc92a8d833819d577010a2ffdd3c58b515896531dce39b3c49b5c2443152d
6
+ metadata.gz: 935badb603ccfa49c812c702fd884f143a130fa051fda3f66fab25080a704549c11df48fc4fb3d731dd3789a1225488d2bd3229a700a5ef0dcab42cb6136bd9f
7
+ data.tar.gz: '008219953c4f5ca5f9e245240bc824c7db35cba780830dee4e2fa58680fe3a8259a87350e4146f42bb91ab838381a51c0b8b899a960338e6eb4bdcd69496861d'
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Erebrus
2
2
 
3
- Modern C/C++ build system like msbuild, with ruby instead of xml
3
+ Modern Ruby based build system like make.
4
4
 
5
5
  ## Usage
6
6
 
data/bin/erebrus CHANGED
@@ -5,147 +5,186 @@ require "erebrus"
5
5
  class ErebrusCommand < Thor
6
6
  class_option :verbose, aliases: "-v", type: :boolean, desc: "Verbose output"
7
7
  class_option :file, aliases: "-f", desc: "Buildfile to use", default: "Buildfile"
8
+ class_option :color, type: :boolean, default: true, desc: "Enable colored output (--no-color to disable)"
9
+ class_option :dir, aliases: "-d", type: :string, desc: "Directory to run the build in"
8
10
 
9
11
  desc "build [TARGET]", "Builds the project with optional target"
10
12
  option :namespace, aliases: "-n", desc: "Target namespace"
11
13
  option :var, aliases: "-D", type: :hash, desc: "Set variables (e.g., -D CC=gcc -D CFLAGS=-O2)"
12
14
  option :parallel, aliases: "-j", type: :numeric, desc: "Number of parallel jobs"
13
15
  def build(target = nil)
14
- load_buildfile_with_error_handling
16
+ in_working_dir do
17
+ load_buildfile_with_error_handling
15
18
 
16
- target_name = resolve_target_name(target)
19
+ target_name = resolve_target_name(target)
17
20
 
18
- context = prepare_context
21
+ context = prepare_context
19
22
 
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
23
+ begin
24
+ Erebrus.build(target_name, context)
25
+ rescue Erebrus::Error => e
26
+ say_error "Build failed: #{e.message}"
27
+ exit 1
28
+ rescue StandardError => e
29
+ say_error "Unexpected error: #{e.message}"
30
+ puts e.backtrace if options[:verbose]
31
+ exit 1
32
+ end
30
33
  end
31
34
  end
32
35
 
33
36
  desc "list [NAMESPACE]", "Lists all available targets, optionally filtered by namespace"
34
37
  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
38
+ in_working_dir do
39
+ load_buildfile_with_error_handling
40
+
41
+ begin
42
+ puts "" # spacing for readability
43
+ if namespace
44
+ say_title("Targets in namespace '#{namespace}'")
45
+ else
46
+ say_title("Available targets")
47
+ end
48
+ if namespace
49
+ Erebrus.list_targets(namespace: namespace)
50
+ else
51
+ Erebrus.list_targets
52
+ end
53
+ rescue StandardError => e
54
+ say_error "Error listing targets: #{e.message}"
55
+ exit 1
42
56
  end
43
- rescue StandardError => e
44
- puts "Error listing targets: #{e.message}"
45
- exit 1
46
57
  end
47
58
  end
48
59
 
49
60
  desc "namespaces", "Lists all available namespaces"
50
61
  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
62
+ in_working_dir do
63
+ load_buildfile_with_error_handling
64
+
65
+ begin
66
+ say_title "Available namespaces"
67
+ Erebrus.list_namespaces
68
+ rescue StandardError => e
69
+ say_error "Error listing namespaces: #{e.message}"
70
+ exit 1
71
+ end
58
72
  end
59
73
  end
60
74
 
61
75
  desc "init [TYPE]", "Initializes the project with a sample Buildfile"
62
76
  def init(type = "basic")
63
- buildfile = options[:file]
77
+ in_working_dir do
78
+ buildfile = options[:file]
64
79
 
65
- if File.exist?(buildfile)
66
- puts "Buildfile '#{buildfile}' already exists!"
67
- return
68
- end
80
+ if File.exist?(buildfile)
81
+ puts "Buildfile '#{buildfile}' already exists!"
82
+ return
83
+ end
69
84
 
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"
85
+ sample_content = case type.downcase
86
+ when "cpp", "c++"
87
+ generate_cpp_buildfile
88
+ when "c"
89
+ generate_c_buildfile
90
+ when "advanced"
91
+ generate_advanced_buildfile
92
+ else
93
+ generate_basic_buildfile
94
+ end
95
+
96
+ File.write(buildfile, sample_content)
97
+ say_success "Created #{buildfile} (#{type} template)"
98
+ say_info "Edit the file to define your build targets and run 'erebrus build' to build your project"
99
+ end
84
100
  end
85
101
 
86
102
  desc "validate", "Validates the Buildfile syntax"
87
103
  def validate
88
- buildfile = options[:file]
104
+ in_working_dir do
105
+ buildfile = options[:file]
89
106
 
90
- unless File.exist?(buildfile)
91
- puts "Error: Buildfile '#{buildfile}' not found"
92
- exit 1
93
- end
107
+ unless File.exist?(buildfile)
108
+ puts "Error: Buildfile '#{buildfile}' not found"
109
+ exit 1
110
+ end
94
111
 
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
112
+ begin
113
+ Erebrus.reset!
114
+ Erebrus.load_buildfile(buildfile)
115
+ say_success "Buildfile is valid \u2713"
116
+ rescue StandardError => e
117
+ say_error "Buildfile validation failed: #{e.message}"
118
+ exit 1
119
+ end
102
120
  end
103
121
  end
104
122
 
105
123
  desc "graph [TARGET]", "Shows dependency graph for target"
106
124
  def graph(target = nil)
107
- load_buildfile_with_error_handling
125
+ in_working_dir do
126
+ load_buildfile_with_error_handling
108
127
 
109
- target_name = resolve_target_name(target)
128
+ target_name = resolve_target_name(target)
110
129
 
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
130
+ begin
131
+ say_title "Dependency graph for '#{target_name}'"
132
+ say_info "(Graph visualization not yet implemented)"
133
+ rescue StandardError => e
134
+ say_error "Error generating graph: #{e.message}"
135
+ exit 1
136
+ end
117
137
  end
118
138
  end
119
139
 
120
140
  desc "clean", "Runs the clean target if available"
121
141
  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
142
+ in_working_dir do
143
+ load_buildfile_with_error_handling
144
+
145
+ begin
146
+ Erebrus.build("clean")
147
+ rescue Erebrus::Error => e
148
+ if e.message.include?("not found")
149
+ say_warn "No clean target defined"
150
+ else
151
+ say_error "Clean failed: #{e.message}"
152
+ exit 1
153
+ end
132
154
  end
133
155
  end
134
156
  end
135
157
 
136
158
  desc "version", "Show version"
137
159
  def version
138
- puts "Erebrus #{Erebrus::VERSION}"
160
+ say_info "Erebrus #{Erebrus::VERSION}"
139
161
  end
140
162
 
141
163
  private
142
164
 
165
+ def in_working_dir
166
+ dir = options[:dir]
167
+ return yield unless dir
168
+
169
+ unless Dir.exist?(dir)
170
+ say_error "Error: directory '#{dir}' not found"
171
+ exit 1
172
+ end
173
+ old = Dir.pwd
174
+ Dir.chdir(dir)
175
+ begin
176
+ yield
177
+ ensure
178
+ Dir.chdir(old)
179
+ end
180
+ end
181
+
143
182
  def load_buildfile_with_error_handling
144
183
  buildfile = options[:file]
145
184
 
146
185
  unless File.exist?(buildfile)
147
- puts "Error: Buildfile '#{buildfile}' not found"
148
- puts "Run 'erebrus init' to create a sample Buildfile"
186
+ say_error "Error: Buildfile '#{buildfile}' not found"
187
+ say_info "Run 'erebrus init' to create a sample Buildfile"
149
188
  exit 1
150
189
  end
151
190
 
@@ -153,7 +192,7 @@ class ErebrusCommand < Thor
153
192
  Erebrus.reset!
154
193
  Erebrus.load_buildfile(buildfile)
155
194
  rescue StandardError => e
156
- puts "Error loading buildfile: #{e.message}"
195
+ say_error "Error loading buildfile: #{e.message}"
157
196
  puts e.backtrace if options[:verbose]
158
197
  exit 1
159
198
  end
@@ -170,6 +209,7 @@ class ErebrusCommand < Thor
170
209
 
171
210
  context.merge!(options[:var]) if options[:var]
172
211
 
212
+ # Legacy variables for backward compatibility
173
213
  context["PLATFORM"] = RUBY_PLATFORM
174
214
  context["RUBY_VERSION"] = RUBY_VERSION
175
215
  context["PWD"] = Dir.pwd
@@ -177,6 +217,57 @@ class ErebrusCommand < Thor
177
217
  context
178
218
  end
179
219
 
220
+ # --- Styling helpers -----------------------------------------------------
221
+ def color_enabled?
222
+ options[:color] && $stdout.respond_to?(:isatty) && $stdout.isatty && ENV["NO_COLOR"].nil?
223
+ rescue StandardError
224
+ options[:color]
225
+ end
226
+
227
+ def color_code(color)
228
+ case color
229
+ when :black then 30
230
+ when :red then 31
231
+ when :green then 32
232
+ when :yellow then 33
233
+ when :blue then 34
234
+ when :magenta then 35
235
+ when :cyan then 36
236
+ when :white then 37
237
+ else nil
238
+ end
239
+ end
240
+
241
+ def c(text, color = nil, bold: false)
242
+ return text unless color_enabled? && (bold || color)
243
+
244
+ codes = []
245
+ codes << 1 if bold
246
+ cc = color_code(color)
247
+ codes << cc if cc
248
+ "\e[#{codes.join(";")}m#{text}\e[0m"
249
+ end
250
+
251
+ def say_title(msg)
252
+ puts c("==> #{msg}", :blue, bold: true)
253
+ end
254
+
255
+ def say_success(msg)
256
+ puts c(msg, :green, bold: true)
257
+ end
258
+
259
+ def say_error(msg)
260
+ puts c(msg, :red, bold: true)
261
+ end
262
+
263
+ def say_warn(msg)
264
+ puts c(msg, :yellow)
265
+ end
266
+
267
+ def say_info(msg)
268
+ puts c(msg, :cyan)
269
+ end
270
+
180
271
  def generate_basic_buildfile
181
272
  <<~BUILDFILE
182
273
  target :clean, description: "Clean build artifacts" do
@@ -0,0 +1,5 @@
1
+ dist/
2
+ obj/
3
+ package/
4
+ build/
5
+ /package
data/examples/Buildfile CHANGED
@@ -1,6 +1,10 @@
1
1
  # Erebrus Buildfile Example
2
2
  # This demonstrates the Ruby build DSL for C/C++ projects
3
3
 
4
+ # Global variables used in the example and templates
5
+ set_variable "APP_NAME", "myapp"
6
+ set_variable "VERSION", "1.0.0"
7
+
4
8
  target :clean, description: "Clean all build artifacts" do
5
9
  remove "build"
6
10
  remove "dist"
@@ -15,44 +19,53 @@ end
15
19
 
16
20
  target :configure, description: "Configure build environment" do
17
21
  depends_on :setup
18
-
22
+
19
23
  action do |context|
20
- puts "Configuring build for #{context[:platform] || 'default'} platform"
24
+ puts "Configuring build for #{context[:platform] || "default"} platform"
21
25
  end
22
26
  end
23
27
 
24
28
  target :compile_sources, description: "Compile source files" do
25
29
  depends_on :configure
26
-
27
- # Compile all C++ files in src/
30
+
28
31
  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
+
33
+ action do
34
+ require "fileutils"
35
+ Dir.glob("*.o").each do |obj|
36
+ FileUtils.mv(obj, File.join("obj", File.basename(obj)))
37
+ end
38
+ end
32
39
  end
33
40
 
34
41
  target :compile_tests, description: "Compile test files" do
35
42
  depends_on :compile_sources
36
-
43
+
37
44
  run "g++ -std=c++17 -Wall -Wextra -O2 -c tests/*.cpp -Iinclude -Isrc"
38
- run "mv *.o obj/ 2>/dev/null || true"
45
+
46
+ action do
47
+ require "fileutils"
48
+ Dir.glob("*.o").each do |obj|
49
+ FileUtils.mv(obj, File.join("obj", File.basename(obj)))
50
+ end
51
+ end
39
52
  end
40
53
 
41
54
  target :link_main, description: "Link main executable" do
42
55
  depends_on :compile_sources
43
-
44
- run "g++ obj/*.o -o dist/myapp"
56
+
57
+ run "g++ obj/main.o obj/hello.o -o dist/myapp"
45
58
  end
46
59
 
47
60
  target :link_tests, description: "Link test executable" do
48
61
  depends_on :compile_tests
49
-
50
- run "g++ obj/*.o -o dist/test_runner"
62
+
63
+ run "g++ obj/test_main.o obj/hello.o -o dist/test_runner"
51
64
  end
52
65
 
53
66
  target :build, description: "Build the main application" do
54
67
  depends_on :link_main
55
-
68
+
56
69
  action do
57
70
  puts "Build completed successfully!"
58
71
  puts "Executable: dist/myapp"
@@ -61,29 +74,65 @@ end
61
74
 
62
75
  target :test, description: "Run all tests" do
63
76
  depends_on :link_tests
64
-
77
+
65
78
  run "dist/test_runner"
66
79
  end
67
80
 
68
81
  target :install, description: "Install the application" do
69
82
  depends_on :build
70
-
71
- copy "dist/myapp", "/usr/local/bin/myapp"
72
-
83
+
84
+ platform :linux do
85
+ directory_exists "/usr/local/bin" do
86
+ copy "dist/myapp", "/usr/local/bin/myapp"
87
+ end
88
+ end
89
+ platform :windows do
90
+ directory_exists "C:/Users/Public" do
91
+ copy "dist/myapp", "C:/Users/Public/myapp.exe"
92
+ end
93
+ end
94
+ platform :darwin do
95
+ directory_exists "/usr/local/bin" do
96
+ copy "dist/myapp", "/usr/local/bin/myapp"
97
+ end
98
+ end
99
+
73
100
  action do
74
- puts "Application installed to /usr/local/bin/myapp"
101
+ puts "Application installed for ${HOST_OS}"
75
102
  end
76
103
  end
77
104
 
78
105
  target :package, description: "Create distribution package" do
79
106
  depends_on :build
80
-
107
+
108
+ # Ensure stale file or directory doesn't block packaging
109
+ remove "package"
81
110
  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 ."
111
+ # Copy executable (handle Windows .exe)
112
+ file_exists "dist/myapp" do
113
+ copy "dist/myapp", "package"
114
+ end
115
+ file_exists "dist/myapp.exe" do
116
+ copy "dist/myapp.exe", "package"
117
+ end
118
+ # Include docs
119
+ file_exists "README.md" do
120
+ copy "README.md", "package"
121
+ end
122
+ file_exists "LICENSE" do
123
+ copy "LICENSE", "package"
124
+ end
125
+
126
+ platform :linux do
127
+ run "tar -czf dist/myapp-1.0.tar.gz -C package ."
128
+ end
129
+ platform :darwin do
130
+ run "tar -czf dist/myapp-1.0.tar.gz -C package ."
131
+ end
132
+ platform :windows do
133
+ echo "Creating zip archive on Windows"
134
+ run "powershell -NoProfile -Command \"Compress-Archive -Path package/* -DestinationPath dist/myapp-1.0.zip -Force\""
135
+ end
87
136
  remove "package"
88
137
  end
89
138
 
@@ -92,5 +141,86 @@ target :all, description: "Build, test, and package" do
92
141
  depends_on :package
93
142
  end
94
143
 
144
+ target :show_info, description: "Show host and environment info" do
145
+ echo "Host OS: ${HOST_OS}"
146
+ echo "Host Architecture: ${HOST_ARCH}"
147
+ echo "CPU Count: ${CPU_COUNT}"
148
+ echo "User: ${USER}"
149
+ echo "Home Directory: ${HOME}"
150
+ echo "Temp Directory: ${TEMP_DIR}"
151
+ echo "Build Time: ${BUILD_TIME}"
152
+ echo "Build Timestamp: ${BUILD_TIMESTAMP}"
153
+ echo "Ruby: ${RUBY_VERSION}"
154
+ echo "PWD: ${PWD}"
155
+ end
156
+
157
+ target :gen_config, description: "Generate config from template" do
158
+ depends_on :setup
159
+ template "templates/app_config.tpl", "dist/config.txt"
160
+ end
161
+
162
+ # Demonstrate write/append utilities
163
+ target :write_demo, description: "Write and append notes" do
164
+ depends_on :setup
165
+ write "dist/notes.txt", "Build Notes\n"
166
+ append "dist/notes.txt", "Host: ${HOST_OS} ${HOST_ARCH}\n"
167
+ append "dist/notes.txt", "User: ${USER}\n"
168
+ append "dist/notes.txt", "Built at: ${BUILD_TIME}\n"
169
+ append "dist/notes.txt", "CPU Count: ${CPU_COUNT}\n"
170
+ end
171
+
172
+ # Demonstrate environment handling
173
+ target :env_demo, description: "Run with temporary environment" do
174
+ with_env("MY_FLAG" => "42") do |ctx|
175
+ run "ruby -e \"puts ENV['MY_FLAG']\""
176
+ end
177
+ end
178
+
179
+ # Demonstrate optional symlink on Unix-like systems
180
+ target :symlink_demo, description: "Create a symlink to myapp (Unix only)" do
181
+ depends_on :build
182
+ platform :linux do
183
+ symlink "dist/myapp", "dist/myapp_link"
184
+ end
185
+ platform :darwin do
186
+ symlink "dist/myapp", "dist/myapp_link"
187
+ end
188
+ end
189
+
190
+ # Parallel demonstration: compile sources and tests concurrently
191
+ target :parallel_compile, description: "Compile sources and tests in parallel" do
192
+ parallel :compile_sources, :compile_tests
193
+ end
194
+
195
+ # Demonstrate conditional logic with system variables
196
+ target :system_specific, description: "Show system-specific behavior" do
197
+ echo "Detected system: ${HOST_OS} on ${HOST_ARCH}"
198
+
199
+ conditional(get_variable("HOST_OS") == "windows") do
200
+ echo "Running Windows-specific commands"
201
+ echo "User profile: ${HOME}"
202
+ echo "Temp directory: ${TEMP_DIR}"
203
+ end
204
+
205
+ conditional(get_variable("HOST_ARCH") == "x64") do
206
+ echo "64-bit architecture detected"
207
+ echo "Using optimized 64-bit settings"
208
+ end
209
+
210
+ # Show CPU-based logic
211
+ echo "System has ${CPU_COUNT} CPU cores available"
212
+ end
213
+
214
+ # Aggregate demo target
215
+ target :demo, description: "Run all demonstration features" do
216
+ depends_on :show_info
217
+ depends_on :gen_config
218
+ depends_on :write_demo
219
+ depends_on :env_demo
220
+ depends_on :system_specific
221
+ depends_on :build
222
+ depends_on :symlink_demo
223
+ end
224
+
95
225
  # Set the default target
96
- default :build
226
+ default :build
@@ -0,0 +1,4 @@
1
+ #pragma once
2
+ #include <string>
3
+
4
+ std::string hello();
@@ -0,0 +1,5 @@
1
+ #include "../include/hello.hpp"
2
+
3
+ std::string hello() {
4
+ return "Hello from Erebrus example";
5
+ }
@@ -0,0 +1,7 @@
1
+ #include <iostream>
2
+ #include "../include/hello.hpp"
3
+
4
+ int main() {
5
+ std::cout << hello() << std::endl;
6
+ return 0;
7
+ }
@@ -0,0 +1,8 @@
1
+ # Application Configuration
2
+ AppName={{APP_NAME}}
3
+ Version={{VERSION}}
4
+ HostOS={{HOST_OS}}
5
+ Binary=dist/myapp
6
+
7
+ # This file is generated by the Erebrus examples Buildfile.
8
+ # You can reference variables defined via set_variable or passed context.
@@ -0,0 +1,10 @@
1
+ #include <iostream>
2
+ #include <cassert>
3
+ #include "../include/hello.hpp"
4
+
5
+ int main() {
6
+ std::string msg = hello();
7
+ assert(msg == "Hello from Erebrus example");
8
+ std::cout << "Test passed: hello() returns expected message" << std::endl;
9
+ return 0;
10
+ }
@@ -1,4 +1,5 @@
1
1
  require "pathname"
2
+ require "rbconfig"
2
3
 
3
4
  module Erebrus
4
5
  class BuildEngine
@@ -14,6 +15,19 @@ module Erebrus
14
15
  @included_files = Set.new
15
16
  @file_watchers = {}
16
17
  @conditional_stack = []
18
+ @progress_total = 0
19
+ @progress_count = 0
20
+ @progress_enabled = false
21
+ @progress_line_open = false
22
+ # Predefine useful system and build variables
23
+ @variables["HOST_OS"] = detect_host_os
24
+ @variables["HOST_ARCH"] = detect_host_arch
25
+ @variables["CPU_COUNT"] = detect_cpu_count
26
+ @variables["USER"] = detect_user
27
+ @variables["HOME"] = detect_home_dir
28
+ @variables["TEMP_DIR"] = detect_temp_dir
29
+ @variables["BUILD_TIME"] = Time.now.strftime("%Y-%m-%d %H:%M:%S")
30
+ @variables["BUILD_TIMESTAMP"] = Time.now.to_i.to_s
17
31
  end
18
32
 
19
33
  def target(name, description: nil, namespace: nil, &block)
@@ -126,8 +140,16 @@ module Erebrus
126
140
 
127
141
  merged_context = @variables.merge(context)
128
142
 
143
+ # Setup build-level progress bar across targets to execute
144
+ target_obj = @targets[target_name]
145
+ to_run = collect_targets_to_run(target_obj)
146
+ @progress_total = to_run.size
147
+ @progress_count = 0
148
+ @progress_enabled = $stdout.respond_to?(:isatty) && $stdout.isatty
149
+
129
150
  reset_all_targets!
130
151
  execute_target(target_obj, merged_context)
152
+ finish_progress_bar if @progress_enabled
131
153
  end
132
154
 
133
155
  def list_targets(namespace: nil)
@@ -218,8 +240,8 @@ module Erebrus
218
240
  return if target.executed
219
241
 
220
242
  unless target.conditions.all? { |condition| condition.call(context) }
221
- puts "Skipping target '#{target_name}' (conditions not met)"
222
243
  target.executed = true
244
+ increment_progress!
223
245
  return
224
246
  end
225
247
 
@@ -233,7 +255,10 @@ module Erebrus
233
255
  execute_target(dep_target, context, visited.dup)
234
256
  end
235
257
 
258
+ # Ensure progress bar line is terminated before any action output
259
+ finish_progress_bar if @progress_enabled
236
260
  target.execute(context)
261
+ increment_progress!
237
262
 
238
263
  visited.delete(target_name)
239
264
  end
@@ -241,6 +266,103 @@ module Erebrus
241
266
  def reset_all_targets!
242
267
  @targets.each_value(&:reset!)
243
268
  end
269
+
270
+ # Progress helpers -------------------------------------------------------
271
+ def collect_targets_to_run(root)
272
+ seen = Set.new
273
+ stack = [root]
274
+ until stack.empty?
275
+ t = stack.pop
276
+ next if seen.include?(t.name)
277
+
278
+ seen.add(t.name)
279
+ t.dependencies.each do |dep|
280
+ dep_name = resolve_target_name(dep)
281
+ dt = @targets[dep_name]
282
+ stack << dt if dt
283
+ end
284
+ end
285
+ seen
286
+ end
287
+
288
+ def increment_progress!
289
+ return unless @progress_enabled
290
+
291
+ @progress_count += 1
292
+ print_progress_bar(@progress_count, @progress_total)
293
+ end
294
+
295
+ def print_progress_bar(current, total)
296
+ current = [current, total].min
297
+ percent = total.zero? ? 100 : ((current.to_f / total) * 100).round
298
+ bar_length = 30
299
+ filled = (percent * bar_length / 100.0).round
300
+ bar = "[" + "#" * filled + " " * (bar_length - filled) + "]"
301
+ $stdout.print "\rProgress #{bar} #{percent}% (#{current}/#{total})"
302
+ $stdout.flush
303
+ @progress_line_open = true
304
+ end
305
+
306
+ def finish_progress_bar
307
+ return unless @progress_enabled
308
+ return unless @progress_line_open
309
+
310
+ $stdout.puts
311
+ @progress_line_open = false
312
+ end
313
+
314
+ private
315
+
316
+ def detect_host_os
317
+ host = RbConfig::CONFIG["host_os"].downcase
318
+ case host
319
+ when /mswin|mingw|cygwin/
320
+ "windows"
321
+ when /darwin|mac os/
322
+ "macos"
323
+ when /linux/
324
+ "linux"
325
+ when /freebsd|openbsd|netbsd/
326
+ "bsd"
327
+ else
328
+ "unknown"
329
+ end
330
+ end
331
+
332
+ def detect_host_arch
333
+ arch = RbConfig::CONFIG["host_cpu"].downcase
334
+ case arch
335
+ when /x86_64|amd64/
336
+ "x64"
337
+ when /i[3-6]86/
338
+ "x86"
339
+ when /arm64|aarch64/
340
+ "arm64"
341
+ when /arm/
342
+ "arm"
343
+ else
344
+ arch
345
+ end
346
+ end
347
+
348
+ def detect_cpu_count
349
+ require "etc"
350
+ Etc.nprocessors.to_s
351
+ rescue StandardError
352
+ "1"
353
+ end
354
+
355
+ def detect_user
356
+ ENV["USER"] || ENV["USERNAME"] || "unknown"
357
+ end
358
+
359
+ def detect_home_dir
360
+ ENV["HOME"] || ENV["USERPROFILE"] || Dir.pwd
361
+ end
362
+
363
+ def detect_temp_dir
364
+ ENV["TMPDIR"] || ENV["TMP"] || ENV["TEMP"] || "/tmp"
365
+ end
244
366
  end
245
367
 
246
368
  class TargetContext
@@ -277,10 +399,83 @@ module Erebrus
277
399
  @target.action(&block)
278
400
  end
279
401
 
402
+ def echo(message)
403
+ @target.action do |context|
404
+ puts expand_variables(message, context)
405
+ end
406
+ end
407
+
408
+ def sleep(seconds)
409
+ @target.action do |context|
410
+ s = expand_variables(seconds, context)
411
+ Kernel.sleep(s.to_f)
412
+ end
413
+ end
414
+
415
+ def move(source, destination)
416
+ @target.action do |context|
417
+ expanded_source = expand_variables(source, context)
418
+ expanded_dest = expand_variables(destination, context)
419
+ require "fileutils"
420
+ FileUtils.mv(expanded_source, expanded_dest)
421
+ end
422
+ end
423
+
424
+ def symlink(target, link_name)
425
+ @target.action do |context|
426
+ expanded_target = expand_variables(target, context)
427
+ expanded_link = expand_variables(link_name, context)
428
+ require "fileutils"
429
+ FileUtils.ln_s(expanded_target, expanded_link, force: true)
430
+ end
431
+ end
432
+
433
+ def write(file, content)
434
+ @target.action do |context|
435
+ expanded_file = expand_variables(file, context)
436
+ expanded_content = expand_variables(content, context)
437
+ File.write(expanded_file, expanded_content)
438
+ end
439
+ end
440
+
441
+ def append(file, content)
442
+ @target.action do |context|
443
+ expanded_file = expand_variables(file, context)
444
+ expanded_content = expand_variables(content, context)
445
+ File.open(expanded_file, "a") { |f| f.write(expanded_content) }
446
+ end
447
+ end
448
+
449
+ def with_env(vars = {})
450
+ @target.action do |context|
451
+ merged = vars.transform_keys(&:to_s).transform_values { |v| expand_variables(v, context).to_s }
452
+ backup = {}
453
+ merged.each do |k, v|
454
+ backup[k] = ENV[k]
455
+ ENV[k] = v
456
+ end
457
+ begin
458
+ yield(context) if block_given?
459
+ ensure
460
+ merged.each_key { |k| ENV[k] = backup[k] }
461
+ end
462
+ end
463
+ end
464
+
465
+ def run_set(name, command)
466
+ @target.action do |context|
467
+ expanded_command = expand_variables(command, context)
468
+ puts "Running (capture): #{expanded_command}"
469
+ result = `#{expanded_command}`
470
+ raise Error, "Command failed: #{expanded_command}" unless $?.success?
471
+
472
+ @engine&.set_variable(name, result.chomp)
473
+ end
474
+ end
475
+
280
476
  def run(command, options = {})
281
477
  @target.action do |context|
282
478
  expanded_command = expand_variables(command, context)
283
- puts "Running: #{expanded_command}"
284
479
 
285
480
  if options[:capture]
286
481
  result = `#{expanded_command}`
@@ -297,8 +492,6 @@ module Erebrus
297
492
  @target.action do |context|
298
493
  expanded_source = expand_variables(source, context)
299
494
  expanded_dest = expand_variables(destination, context)
300
-
301
- puts "Copying #{expanded_source} to #{expanded_dest}"
302
495
  require "fileutils"
303
496
 
304
497
  if options[:preserve]
@@ -312,7 +505,6 @@ module Erebrus
312
505
  def mkdir(path)
313
506
  @target.action do |context|
314
507
  expanded_path = expand_variables(path, context)
315
- puts "Creating directory: #{expanded_path}"
316
508
  require "fileutils"
317
509
  FileUtils.mkdir_p(expanded_path)
318
510
  end
@@ -321,7 +513,6 @@ module Erebrus
321
513
  def remove(path)
322
514
  @target.action do |context|
323
515
  expanded_path = expand_variables(path, context)
324
- puts "Removing: #{expanded_path}"
325
516
  require "fileutils"
326
517
  FileUtils.rm_rf(expanded_path)
327
518
  end
@@ -330,7 +521,6 @@ module Erebrus
330
521
  def touch(path)
331
522
  @target.action do |context|
332
523
  expanded_path = expand_variables(path, context)
333
- puts "Touching: #{expanded_path}"
334
524
  require "fileutils"
335
525
  FileUtils.touch(expanded_path)
336
526
  end
@@ -339,7 +529,6 @@ module Erebrus
339
529
  def chmod(mode, path)
340
530
  @target.action do |context|
341
531
  expanded_path = expand_variables(path, context)
342
- puts "Changing permissions of #{expanded_path} to #{mode}"
343
532
  require "fileutils"
344
533
  FileUtils.chmod(mode, expanded_path)
345
534
  end
@@ -349,8 +538,6 @@ module Erebrus
349
538
  @target.action do |context|
350
539
  expanded_url = expand_variables(url, context)
351
540
  expanded_dest = expand_variables(destination, context)
352
-
353
- puts "Downloading #{expanded_url} to #{expanded_dest}"
354
541
  require "net/http"
355
542
  require "uri"
356
543
 
@@ -371,8 +558,6 @@ module Erebrus
371
558
  expanded_template = expand_variables(template_file, context)
372
559
  expanded_output = expand_variables(output_file, context)
373
560
 
374
- puts "Processing template #{expanded_template} -> #{expanded_output}"
375
-
376
561
  template_content = File.read(expanded_template)
377
562
  merged_vars = context.merge(variables)
378
563
 
@@ -75,15 +75,8 @@ module Erebrus
75
75
 
76
76
  start_time = Time.now
77
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
78
  begin
84
79
  @actions.each_with_index do |action, index|
85
- puts " Action #{index + 1}/#{@actions.size}" if @actions.size > 1
86
-
87
80
  if action.arity == 0
88
81
  action.call
89
82
  else
@@ -93,11 +86,7 @@ module Erebrus
93
86
 
94
87
  @executed = true
95
88
  @last_run = Time.now
96
-
97
- execution_time = Time.now - start_time
98
- puts " Completed in #{execution_time.round(2)}s"
99
89
  rescue StandardError => e
100
- puts " Failed: #{e.message}"
101
90
  raise Error, "Target '#{@name}' failed: #{e.message}"
102
91
  end
103
92
  end
@@ -1,3 +1,3 @@
1
1
  module Erebrus
2
- VERSION = "0.1.0"
3
- end
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,78 @@
1
+ module Erebrus
2
+ class BuildEngine
3
+ attr_reader targets: Hash[String, Erebrus::Target]
4
+ attr_reader namespaces: Hash[String, Array[String]]
5
+ attr_reader variables: Hash[String, untyped]
6
+ attr_reader included_files: ::Set[String]
7
+
8
+ attr_accessor current_namespace: String?
9
+
10
+ def initialize: () -> void
11
+
12
+ def target: ((String | Symbol) name, description?: String?, namespace?: String?, ?{ () -> void }) -> Erebrus::Target
13
+ def namespace: ((String | Symbol) name) { () -> void } -> void
14
+ def include_buildfile: (String file_path, namespace?: String?) -> void
15
+
16
+ def set_variable: ((String | Symbol) name, untyped value) -> void
17
+ def get_variable: ((String | Symbol) name, untyped? default) -> untyped
18
+
19
+ def conditional: (untyped condition) { () -> void } -> void
20
+
21
+ def platform?: ((String | Symbol) name) -> bool
22
+ def file_exists?: (String path) -> bool
23
+ def directory_exists?: (String path) -> bool
24
+
25
+ def default: ((String | Symbol) target_name) -> void
26
+ def build: (? (String | Symbol) target_name, ?Hash[Symbol | String, untyped] context) -> void
27
+
28
+ def list_targets: (namespace?: String) -> void
29
+ def list_namespaces: () -> void
30
+
31
+ def watch_file: (String pattern) { (String) -> untyped } -> void
32
+ def check_file_changes: () -> void
33
+ end
34
+
35
+ class TargetContext
36
+ def initialize: (Erebrus::Target target, ?Erebrus::BuildEngine engine) -> void
37
+
38
+ def depends_on: (* (String | Symbol) deps) -> Erebrus::Target
39
+ def depends_on_files: (* String files) -> Erebrus::Target
40
+ def produces: (* String files) -> Erebrus::Target
41
+
42
+ def condition: () ?{ (Hash[Symbol | String, untyped]) -> bool } -> Erebrus::Target
43
+ def only_if: ((::Proc | Symbol | String | bool) condition) -> Erebrus::Target
44
+
45
+ def tag: (* (String | Symbol) tags) -> Erebrus::Target
46
+
47
+ def action: () { () -> untyped } -> Erebrus::Target
48
+ | () { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
49
+ | () -> Erebrus::Target
50
+
51
+ def run: (String command, Hash[Symbol, untyped]? options) -> Erebrus::Target
52
+ def copy: (String source, String destination, Hash[Symbol, untyped]? options) -> Erebrus::Target
53
+ def mkdir: (String path) -> Erebrus::Target
54
+ def remove: (String path) -> Erebrus::Target
55
+ def touch: (String path) -> Erebrus::Target
56
+ def chmod: (Integer mode, String path) -> Erebrus::Target
57
+ def download: (String url, String destination) -> Erebrus::Target
58
+ def template: (String template_file, String output_file, Hash[Symbol | String, untyped]? variables) -> Erebrus::Target
59
+ def parallel: (* (String | Symbol) targets) -> Erebrus::Target
60
+
61
+ def conditional: (untyped condition) { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
62
+ def platform: ((String | Symbol) name) { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
63
+ def file_exists: (String path) { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
64
+ def directory_exists: (String path) { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
65
+
66
+ def set_variable: ((String | Symbol) name, untyped value) -> Erebrus::Target
67
+ def get_variable: ((String | Symbol) name, untyped? default) -> untyped
68
+
69
+ def echo: (String message) -> Erebrus::Target
70
+ def sleep: ((Integer | Float | String) seconds) -> Erebrus::Target
71
+ def move: (String source, String destination) -> Erebrus::Target
72
+ def symlink: (String target, String link_name) -> Erebrus::Target
73
+ def write: (String file, String content) -> Erebrus::Target
74
+ def append: (String file, String content) -> Erebrus::Target
75
+ def with_env: (Hash[Symbol | String, untyped] vars) { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
76
+ def run_set: ((String | Symbol) name, String command) -> Erebrus::Target
77
+ end
78
+ end
@@ -0,0 +1,37 @@
1
+ module Erebrus
2
+ module DSL
3
+ def self.included: (untyped base) -> void
4
+
5
+ module ClassMethods
6
+ def build_engine: () -> Erebrus::BuildEngine
7
+
8
+ def target: ((String | Symbol) name, description?: String?, namespace?: String?, ?{ () -> void }) -> Erebrus::Target
9
+ def namespace: ((String | Symbol) name) { () -> void } -> void
10
+ def include_buildfile: (String file_path, namespace?: String?) -> void
11
+
12
+ def set_variable: ((String | Symbol) name, untyped value) -> void
13
+ def get_variable: ((String | Symbol) name, untyped? default) -> untyped
14
+
15
+ def conditional: (untyped condition) { () -> void } -> void
16
+ def platform?: ((String | Symbol) name) -> bool
17
+ def default: ((String | Symbol) target_name) -> void
18
+ def build: (? (String | Symbol) target_name, ?Hash[Symbol | String, untyped] context) -> void
19
+ def list_targets: (namespace?: String) -> void
20
+ def list_namespaces: () -> void
21
+ end
22
+
23
+ def target: ((String | Symbol) name, description?: String?, namespace?: String?, ?{ () -> void }) -> Erebrus::Target
24
+ def namespace: ((String | Symbol) name) { () -> void } -> void
25
+ def include_buildfile: (String file_path, namespace?: String?) -> void
26
+
27
+ def set_variable: ((String | Symbol) name, untyped value) -> void
28
+ def get_variable: ((String | Symbol) name, untyped? default) -> untyped
29
+
30
+ def conditional: (untyped condition) { () -> void } -> void
31
+ def platform?: ((String | Symbol) name) -> bool
32
+ def default: ((String | Symbol) target_name) -> void
33
+ def build: (? (String | Symbol) target_name, ?Hash[Symbol | String, untyped] context) -> void
34
+ def list_targets: (namespace?: String) -> void
35
+ def list_namespaces: () -> void
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ module Erebrus
2
+ class Target
3
+ attr_reader name: String
4
+ attr_reader dependencies: Array[String]
5
+ attr_reader description: String?
6
+ attr_reader conditions: Array[::Proc]
7
+ attr_reader file_dependencies: Array[String]
8
+ attr_reader outputs: Array[String]
9
+
10
+ attr_accessor executed: bool
11
+ attr_accessor namespace: String?
12
+ attr_accessor tags: Array[String]
13
+ attr_accessor priority: Integer
14
+
15
+ def initialize: ((String | Symbol) name, ?description: String?) -> void
16
+
17
+ def depends_on: (* (String | Symbol) deps) -> Erebrus::Target
18
+ def depends_on_files: (* String files) -> Erebrus::Target
19
+ def produces: (* String files) -> Erebrus::Target
20
+
21
+ def condition: () ?{ (Hash[Symbol | String, untyped]) -> bool } -> Erebrus::Target
22
+ def only_if: ((::Proc | Symbol | String | bool) condition) -> Erebrus::Target
23
+
24
+ def tag: (* (String | Symbol) tags) -> Erebrus::Target
25
+
26
+ def action: () { () -> untyped } -> Erebrus::Target
27
+ | () { (Hash[Symbol | String, untyped]) -> untyped } -> Erebrus::Target
28
+ | () -> Erebrus::Target
29
+
30
+ def needs_execution?: (?Hash[Symbol | String, untyped]) -> bool
31
+ def execute: (?Hash[Symbol | String, untyped]) -> void
32
+ def reset!: () -> void
33
+ def file_dependencies_changed?: () -> bool
34
+ def to_s: () -> String
35
+ def inspect: () -> String
36
+ end
37
+ end
data/sig/erebrus.rbs CHANGED
@@ -1,4 +1,23 @@
1
1
  module Erebrus
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class Error < ::StandardError
5
+ end
6
+
7
+ def self.target: ((String | Symbol) name, description?: String?, namespace?: String?, ?{ () -> void }) -> Erebrus::Target
8
+ def self.namespace: ((String | Symbol) name) { () -> void } -> void
9
+ def self.include_buildfile: (String file_path, namespace?: String?) -> void
10
+
11
+ def self.set_variable: ((String | Symbol) name, untyped value) -> void
12
+ def self.get_variable: ((String | Symbol) name, untyped? default) -> untyped
13
+
14
+ def self.conditional: (untyped condition) { () -> void } -> void
15
+ def self.platform?: ((String | Symbol) name) -> bool
16
+ def self.default: ((String | Symbol) target_name) -> void
17
+ def self.build: (? (String | Symbol) target_name, ?Hash[Symbol | String, untyped] context) -> void
18
+ def self.list_targets: (namespace?: String) -> void
19
+ def self.list_namespaces: () -> void
20
+
21
+ def self.load_buildfile: (String file_path) -> void
22
+ def self.reset!: () -> void
4
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erebrus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - jel9
@@ -34,13 +34,22 @@ files:
34
34
  - README.md
35
35
  - Rakefile
36
36
  - bin/erebrus
37
+ - examples/.gitignore
37
38
  - examples/Buildfile
39
+ - examples/include/hello.hpp
40
+ - examples/src/hello.cpp
41
+ - examples/src/main.cpp
42
+ - examples/templates/app_config.tpl
43
+ - examples/tests/test_main.cpp
38
44
  - lib/erebrus.rb
39
45
  - lib/erebrus/build_engine.rb
40
46
  - lib/erebrus/dsl.rb
41
47
  - lib/erebrus/target.rb
42
48
  - lib/erebrus/version.rb
43
49
  - sig/erebrus.rbs
50
+ - sig/erebrus/build_engine.rbs
51
+ - sig/erebrus/dsl.rbs
52
+ - sig/erebrus/target.rbs
44
53
  homepage: https://github.com/daedalus-os/erebrus
45
54
  licenses: []
46
55
  metadata:
@@ -62,5 +71,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
71
  requirements: []
63
72
  rubygems_version: 3.6.9
64
73
  specification_version: 4
65
- summary: Modern C/C++ build system like msbuild, with ruby instead of xml
74
+ summary: Modern Ruby based build system like make.
66
75
  test_files: []