rake-builder 0.0.8

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.
@@ -0,0 +1,447 @@
1
+ require 'rubygems' if RUBY_VERSION < '1.9'
2
+ require 'logger'
3
+ require 'rake'
4
+ require 'rake/tasklib'
5
+ require 'rake/loaders/makefile'
6
+
7
+ module Rake
8
+
9
+ # A task whose behaviour depends on a FileTask
10
+ class FileTaskAlias < Task
11
+
12
+ attr_accessor :target
13
+
14
+ def self.define_task( name, target, &block )
15
+ alias_task = super( { name => [] }, &block )
16
+ alias_task.target = target
17
+ alias_task.prerequisites.unshift( target )
18
+ alias_task
19
+ end
20
+
21
+ def needed?
22
+ Rake::Task[ @target ].needed?
23
+ end
24
+
25
+ end
26
+
27
+ # Error indicating that the project failed to build.
28
+ class BuildFailureError < StandardError
29
+ end
30
+
31
+ class Builder < TaskLib
32
+
33
+ module VERSION #:nodoc:
34
+ MAJOR = 0
35
+ MINOR = 0
36
+ TINY = 8
37
+
38
+ STRING = [ MAJOR, MINOR, TINY ].join('.')
39
+ end
40
+
41
+ # Expand path to an absolute path relative to the supplied root
42
+ def self.expand_path_with_root( path, root )
43
+ if path =~ /^\//
44
+ File.expand_path( path )
45
+ else
46
+ File.expand_path( root + '/' + path )
47
+ end
48
+ end
49
+
50
+ # Expand an array of paths to absolute paths relative to the supplied root
51
+ def self.expand_paths_with_root( paths, root )
52
+ paths.map{ |path| expand_path_with_root( path, root ) }
53
+ end
54
+
55
+ # The file to be built
56
+ attr_accessor :target
57
+
58
+ # The type of file to be built
59
+ # One of: :executable, :static_library, :shared_library
60
+ # If not set, this is deduced from the target.
61
+ attr_accessor :target_type
62
+
63
+ # The types of file that can be built
64
+ TARGET_TYPES = [ :executable, :static_library, :shared_library ]
65
+
66
+ # The programming language: 'c++' or 'c' (default 'c++')
67
+ # This also sets defaults for source_file_extension
68
+ attr_accessor :programming_language
69
+
70
+ # Programmaing languages that Rake::Builder can handle
71
+ KNOWN_LANGUAGES = {
72
+ 'c' => {
73
+ :source_file_extension => 'c',
74
+ :compiler => 'gcc',
75
+ :linker => 'gcc'
76
+ },
77
+ 'c++' => {
78
+ :source_file_extension => 'cpp',
79
+ :compiler => 'g++',
80
+ :linker => 'g++'
81
+ },
82
+ 'objective-c' => {
83
+ :source_file_extension => 'm',
84
+ :compiler => 'gcc',
85
+ :linker => 'gcc'
86
+ },
87
+ }
88
+
89
+ # The compiler that will be used
90
+ attr_accessor :compiler
91
+
92
+ # The linker that will be used
93
+ attr_accessor :linker
94
+
95
+ # Extension of source files (default 'cpp' for C++ and 'c' fo C)
96
+ attr_accessor :source_file_extension
97
+
98
+ # Extension of header files (default 'h')
99
+ attr_accessor :header_file_extension
100
+
101
+ # The path of the Rakefile
102
+ # All paths are relative to this
103
+ attr_reader :rakefile_path
104
+
105
+ # The Rakefile
106
+ # The file is not necessarily called 'Rakefile'
107
+ # It is the file which calls to Rake::Builder.new
108
+ attr_reader :rakefile
109
+
110
+ # Directories containing project source files
111
+ attr_accessor :source_search_paths
112
+
113
+ # Directories containing project header files
114
+ attr_accessor :header_search_paths
115
+
116
+ # (Optional) namespace for tasks
117
+ attr_accessor :task_namespace
118
+
119
+ # Name of the default task
120
+ attr_accessor :default_task
121
+
122
+ # Tasks which the target file depends upon
123
+ attr_accessor :target_prerequisites
124
+
125
+ # Directory to be used for object files
126
+ attr_accessor :objects_path
127
+
128
+ # extra options to pass to the compiler
129
+ attr_accessor :compilation_options
130
+
131
+ # Additional include directories for compilation
132
+ attr_accessor :include_paths
133
+
134
+ # Additional library directories for linking
135
+ attr_accessor :library_paths
136
+
137
+ # Libraries to be linked
138
+ attr_accessor :library_dependencies
139
+
140
+ # The directory where 'rake install' will copy the target file
141
+ attr_accessor :install_path
142
+
143
+ # Name of the generated file containing source - header dependencies
144
+ attr_reader :makedepend_file
145
+
146
+ # Temporary files generated during compilation and linking
147
+ attr_accessor :generated_files
148
+
149
+ # Each instance has its own logger
150
+ attr_accessor :logger
151
+
152
+ def initialize( &block )
153
+ save_rakefile_info( caller[0] )
154
+ initialize_attributes
155
+ block.call( self )
156
+ configure
157
+ define_tasks
158
+ define_default
159
+ end
160
+
161
+ # Source files found in source_search_paths
162
+ def source_files
163
+ @source_fies ||= find_files( @source_search_paths, @source_file_extension )
164
+ end
165
+
166
+ # Header files found in header_search_paths
167
+ def header_files
168
+ @header_files ||= find_files( @header_search_paths, @header_file_extension )
169
+ end
170
+
171
+ private
172
+
173
+ def initialize_attributes
174
+ @logger = Logger.new( STDOUT )
175
+ @logger.level = Logger::UNKNOWN
176
+ @programming_language = 'c++'
177
+ @header_file_extension = 'h'
178
+ @objects_path = @rakefile_path.dup
179
+ @generated_files = []
180
+ @library_paths = []
181
+ @library_dependencies = []
182
+ @target_prerequisites = []
183
+ @source_search_paths = [ @rakefile_path.dup ]
184
+ @header_search_paths = [ @rakefile_path.dup ]
185
+ @target = 'a.out'
186
+ @generated_files = []
187
+ end
188
+
189
+ def configure
190
+ @programming_language.downcase!
191
+ raise "Don't know how to build '#{ @programming_language }' programs" if KNOWN_LANGUAGES[ @programming_language ].nil?
192
+ @compiler ||= KNOWN_LANGUAGES[ @programming_language ][ :compiler ]
193
+ @linker ||= KNOWN_LANGUAGES[ @programming_language ][ :linker ]
194
+ @source_file_extension ||= KNOWN_LANGUAGES[ @programming_language ][ :source_file_extension ]
195
+
196
+ @source_search_paths = Rake::Builder.expand_paths_with_root( @source_search_paths, @rakefile_path )
197
+ @header_search_paths = Rake::Builder.expand_paths_with_root( @header_search_paths, @rakefile_path )
198
+ @library_paths = Rake::Builder.expand_paths_with_root( @library_paths, @rakefile_path )
199
+
200
+ raise "The target name cannot be nil" if @target.nil?
201
+ raise "The target name cannot be an empty string" if @target == ''
202
+ @objects_path = Rake::Builder.expand_path_with_root( @objects_path, @rakefile_path )
203
+ @target = Rake::Builder.expand_path_with_root( @target, @objects_path )
204
+ @target_type ||= type( @target )
205
+ raise "Building #{ @target_type } targets is not supported" if ! TARGET_TYPES.include?( @target_type )
206
+ @install_path ||= default_install_path( @target_type )
207
+
208
+ @compilation_options ||= ''
209
+ @include_paths ||= @header_search_paths.dup
210
+ @include_paths = Rake::Builder.expand_paths_with_root( @include_paths, @rakefile_path )
211
+ @generated_files = Rake::Builder.expand_paths_with_root( @generated_files, @rakefile_path )
212
+
213
+ @default_task ||= :build
214
+ @target_prerequisites << @rakefile
215
+
216
+ @makedepend_file = @objects_path + '/.' + target_basename + '.depend.mf'
217
+
218
+ raise "No source files found" if source_files.length == 0
219
+ end
220
+
221
+ def define_tasks
222
+ if @task_namespace
223
+ namespace @task_namespace do
224
+ define
225
+ end
226
+ else
227
+ define
228
+ end
229
+ end
230
+
231
+ def define_default
232
+ name = scoped_task( @default_task )
233
+ desc "Equivalent to 'rake #{ name }'"
234
+ if @task_namespace
235
+ task @task_namespace => [ name ]
236
+ else
237
+ task :default => [ name ]
238
+ end
239
+ end
240
+
241
+ def define
242
+ if @target_type == :executable
243
+ desc "Run '#{ target_basename }'"
244
+ task :run => :build do
245
+ command = "cd #{ @rakefile_path } && #{ @target }"
246
+ puts shell( command, Logger::INFO )
247
+ end
248
+ end
249
+
250
+ desc "Compile and build '#{ target_basename }'"
251
+ FileTaskAlias.define_task( :build, @target )
252
+
253
+ desc "Build '#{ target_basename }'"
254
+ file @target => [ scoped_task( :compile ), @target_prerequisites ] do |t|
255
+ shell "rm -f #{ t.name }"
256
+ case @target_type
257
+ when :executable
258
+ shell "#{ @linker } #{ link_flags } -o #{ @target } #{ file_list( object_files ) }"
259
+ when :static_library
260
+ @logger.add( Logger::INFO, "Builing library '#{ t.name }'" )
261
+ shell "ar -cq #{ t.name } #{ file_list( object_files ) }"
262
+ when :shared_library
263
+ @logger.add( Logger::INFO, "Builing library '#{ t.name }'" )
264
+ shell "#{ @linker } -shared -o #{ t.name } #{ file_list( object_files ) } #{ link_flags }"
265
+ end
266
+ raise BuildFailureError if ! File.exist?( t.name )
267
+ end
268
+
269
+ desc "Compile all sources"
270
+ # Only import dependencies when we're compiling
271
+ # otherwise makedepend gets run on e.g. 'rake -T'
272
+ task :compile => [ @makedepend_file, scoped_task( :load_makedepend ), *object_files ]
273
+
274
+ source_files.each do |src|
275
+ object = object_path( src )
276
+ @generated_files << object
277
+ file object => [ src ] do |t|
278
+ @logger.add( Logger::INFO, "Compiling '#{ src }'" )
279
+ shell "#{ @compiler } #{ compiler_flags } -c -o #{ object } #{ src }"
280
+ end
281
+ end
282
+
283
+ file @makedepend_file => [ *project_files ] do
284
+ @logger.add( Logger::DEBUG, "Analysing dependencies" )
285
+ command = "makedepend -f- -- #{ include_path } -- #{ file_list( source_files ) } 2>/dev/null > #{ @makedepend_file }"
286
+ shell command
287
+ end
288
+
289
+ task :load_makedepend => @makedepend_file do |t|
290
+ object_to_source = source_files.inject( {} ) do |memo, source|
291
+ mapped_object = source.gsub( '.' + @source_file_extension, '.o' )
292
+ memo[ mapped_object ] = source
293
+ memo
294
+ end
295
+ File.open( @makedepend_file ).each_line do |line|
296
+ next if line !~ /:\s/
297
+ mapped_object_file = $`
298
+ header_file = $'.gsub( "\n", '' )
299
+ # Why does it work
300
+ # if I make the object (not the source) depend on the header
301
+ source_file = object_to_source[ mapped_object_file ]
302
+ object_file = object_path( source_file )
303
+ object_file_task = Rake.application[ object_file ]
304
+ object_file_task.enhance( [ header_file ] )
305
+ end
306
+ end
307
+
308
+ desc 'List generated files (which are remove with \'rake clean\')'
309
+ task :generated_files do
310
+ puts @generated_files.inspect
311
+ end
312
+
313
+ # Re-implement :clean locally for project and within namespace
314
+ # Standard :clean is a singleton
315
+ desc "Remove temporary files"
316
+ task :clean do
317
+ @generated_files.each do |file|
318
+ shell "rm -f #{ file }"
319
+ end
320
+ end
321
+
322
+ @generated_files << @target
323
+ @generated_files << @makedepend_file
324
+
325
+ desc "Install '#{ target_basename }' in '#{ @install_path }'"
326
+ task :install, [] => [ scoped_task( :build ) ] do
327
+ destination = File.join( @install_path, target_basename )
328
+ begin
329
+ shell "cp '#{ @target }' '#{ destination }'", Logger::INFO
330
+ rescue Errno::EACCES => e
331
+ raise "You do not have premission to install '#{ target_basename }' in '#{ @install_path }'\nTry\n $ sudo rake install"
332
+ end
333
+ end
334
+
335
+ desc "Uninstall '#{ target_basename }' from '#{ @install_path }'"
336
+ task :uninstall, [] => [] do
337
+ destination = File.join( @install_path, File.basename( @target ) )
338
+ if ! File.exist?( destination )
339
+ @logger.add( Logger::INFO, "The file '#{ destination }' does not exist" )
340
+ next
341
+ end
342
+ begin
343
+ shell "rm '#{ destination }'", Logger::INFO
344
+ rescue Errno::EACCES => e
345
+ raise "You do not have premission to uninstall '#{ destination }'\nTry\n $ sudo rake uninstall"
346
+ end
347
+ end
348
+
349
+ end
350
+
351
+ def scoped_task( task )
352
+ if @task_namespace
353
+ "#{ task_namespace }:#{ task }"
354
+ else
355
+ task
356
+ end
357
+ end
358
+
359
+ def type( target )
360
+ case target
361
+ when /\.a/
362
+ :static_library
363
+ when /\.so/
364
+ :shared_library
365
+ else
366
+ :executable
367
+ end
368
+ end
369
+
370
+ # Compiling and linking parameters
371
+
372
+ def include_path
373
+ @include_paths.map { |p| "-I#{ p }" }.join( " " )
374
+ end
375
+
376
+ def compiler_flags
377
+ include_path + ' ' + @compilation_options
378
+ end
379
+
380
+ def link_flags
381
+ [ library_paths_list, library_dependencies_list ].join( " " )
382
+ end
383
+
384
+ # Paths
385
+
386
+ def save_rakefile_info( caller )
387
+ @rakefile = caller.match(/^([^\:]+)/)[1]
388
+ @rakefile_path = File.expand_path( File.dirname( @rakefile ) )
389
+ end
390
+
391
+ def object_path( source_path_name )
392
+ o_name = File.basename( source_path_name ).gsub( '.' + @source_file_extension, '.o' )
393
+ Rake::Builder.expand_path_with_root( o_name, @objects_path )
394
+ end
395
+
396
+ def default_install_path( target_type )
397
+ case target_type
398
+ when :executable
399
+ '/usr/local/bin'
400
+ else
401
+ '/usr/local/lib'
402
+ end
403
+ end
404
+
405
+ def target_basename
406
+ File.basename( @target )
407
+ end
408
+
409
+ # Lists of files
410
+
411
+ def find_files( paths, extension )
412
+ files = paths.reduce( [] ) do |memo, path|
413
+ glob = ( path =~ /[\*\?]/ ) ? path : path + '/*.' + extension
414
+ memo + FileList[ glob ]
415
+ end
416
+ Rake::Builder.expand_paths_with_root( files, @rakefile_path )
417
+ end
418
+
419
+ # TODO: make this return a FileList, not a plain Array
420
+ def object_files
421
+ source_files.map { |file| object_path( file ) }
422
+ end
423
+
424
+ def project_files
425
+ source_files + header_files
426
+ end
427
+
428
+ def file_list( files )
429
+ files.join( " " )
430
+ end
431
+
432
+ def library_paths_list
433
+ @library_paths.map { |l| "-L#{ l }" }.join( " " )
434
+ end
435
+
436
+ def library_dependencies_list
437
+ @library_dependencies.map { |l| "-l#{ l }" }.join( " " )
438
+ end
439
+
440
+ def shell( command, log_level = Logger::ERROR )
441
+ @logger.add( log_level, command )
442
+ `#{ command }`
443
+ end
444
+
445
+ end
446
+
447
+ end
@@ -0,0 +1,12 @@
1
+ #include "main.h"
2
+
3
+ int main( int argc, char *argv[] ) {
4
+ FILE * file = fopen ( "rake-c-testfile.txt", "w" );
5
+ if( file == NULL )
6
+ return 1;
7
+
8
+ fputs( "rake-builder test", file );
9
+ fclose( file );
10
+
11
+ return 0;
12
+ }
@@ -0,0 +1,6 @@
1
+ #ifndef __PROJECT_MAIN_H__
2
+ #define __PROJECT_MAIN_H__
3
+
4
+ #include <stdio.h>
5
+
6
+ #endif // ndef __PROJECT_MAIN_H__
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe 'when building a C project' do
4
+
5
+ include RakeBuilderHelper
6
+
7
+ before( :all ) do
8
+ @test_output_file = Rake::Builder.expand_path_with_root( 'rake-c-testfile.txt', SPEC_PATH )
9
+ end
10
+
11
+ before( :each ) do
12
+ Rake::Task.clear
13
+ @project = c_task( :executable )
14
+ @expected_generated = Rake::Builder.expand_paths_with_root( [ './main.o', @project.makedepend_file, @project.target ], SPEC_PATH )
15
+ `rm -f #{ @test_output_file }`
16
+ `rm -f #{ @project.target }`
17
+ end
18
+
19
+ after( :each ) do
20
+ Rake::Task[ 'clean' ].invoke
21
+ `rm -f #{ @test_output_file }`
22
+ end
23
+
24
+ it 'builds the program with \'build\'' do
25
+ Rake::Task[ 'build' ].invoke
26
+ exist?( @project.target ).should be_true
27
+ end
28
+
29
+ it 'runs the program with \'run\'' do
30
+ Rake::Task[ 'run' ].invoke
31
+ exist?( @test_output_file ).should be_true
32
+ end
33
+
34
+ end
@@ -0,0 +1,12 @@
1
+ #include "main.h"
2
+
3
+ int main( int argc, char *argv[] ) {
4
+ ofstream outfile( "rake-builder-testfile.txt" );
5
+
6
+ if( outfile.fail() )
7
+ return 1;
8
+
9
+ outfile << "rake-builder test";
10
+
11
+ return 0;
12
+ }
@@ -0,0 +1,8 @@
1
+ #ifndef __PROJECT_MAIN_H__
2
+ #define __PROJECT_MAIN_H__
3
+
4
+ #include <iostream>
5
+ #include <fstream>
6
+ using namespace std;
7
+
8
+ #endif // ndef __PROJECT_MAIN_H__
@@ -0,0 +1,186 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe 'when building an executable' do
4
+
5
+ include RakeBuilderHelper
6
+
7
+ before( :all ) do
8
+ @test_output_file = Rake::Builder.expand_path_with_root( 'rake-builder-testfile.txt', SPEC_PATH )
9
+ @expected_target = Rake::Builder.expand_path_with_root(
10
+ RakeBuilderHelper::TARGET[ :executable ],
11
+ SPEC_PATH
12
+ )
13
+ end
14
+
15
+ before( :each ) do
16
+ Rake::Task.clear
17
+ @project = cpp_task( :executable )
18
+ `rm -f #{ @test_output_file }`
19
+ `rm -f #{ @project.target }`
20
+ end
21
+
22
+ after( :each ) do
23
+ Rake::Task[ 'clean' ].invoke
24
+ `rm -f #{ @test_output_file }`
25
+ end
26
+
27
+ it 'knows the target' do
28
+ @project.target.should == @expected_target
29
+ end
30
+
31
+ it 'builds the target in the objects directory' do
32
+ File.dirname( @project.target ).should == @project.objects_path
33
+ end
34
+
35
+ it 'knows the project type' do
36
+ @project.target_type.should == :executable
37
+ end
38
+
39
+ it 'creates the correct tasks' do
40
+ expected_tasks = expected_tasks( [ @project.target ] )
41
+ missing_tasks = expected_tasks - task_names
42
+ missing_tasks.should == []
43
+ end
44
+
45
+ it 'finds source files' do
46
+ expected_sources = Rake::Builder.expand_paths_with_root( [ 'cpp_project/main.cpp' ], SPEC_PATH )
47
+ @project.source_files.should == expected_sources
48
+ end
49
+
50
+ it 'finds header files' do
51
+ expected_headers = Rake::Builder.expand_paths_with_root( [ 'cpp_project/main.h' ], SPEC_PATH )
52
+ @project.header_files.should == expected_headers
53
+ end
54
+
55
+ it 'builds the program with \'build\'' do
56
+ Rake::Task[ 'build' ].invoke
57
+ exist?( @project.target ).should be_true
58
+ end
59
+
60
+ it 'has a \'run\' task' do
61
+ Rake::Task[ 'run' ].should_not be_nil
62
+ end
63
+
64
+ it 'builds the program with \'run\'' do
65
+ Rake::Task[ 'run' ].invoke
66
+ exist?( @project.target ).should be_true
67
+ end
68
+
69
+ it 'runs the program with \'run\'' do
70
+ Rake::Task[ 'run' ].invoke
71
+ exist?( @test_output_file ).should be_true
72
+ end
73
+
74
+ end
75
+
76
+ describe 'when using namespaces' do
77
+
78
+ include RakeBuilderHelper
79
+
80
+ before( :each ) do
81
+ Rake::Task.clear
82
+ @project = cpp_task( :executable, 'my_namespace' )
83
+ end
84
+
85
+ after( :each ) do
86
+ Rake::Task[ 'my_namespace:clean' ].invoke
87
+ end
88
+
89
+ it 'creates the correct tasks' do
90
+ expected_tasks = expected_tasks( [ @project.target ], 'my_namespace' )
91
+ missing_tasks = expected_tasks - task_names
92
+ missing_tasks.should == []
93
+ end
94
+
95
+ end
96
+
97
+ describe 'when building a static library' do
98
+
99
+ include RakeBuilderHelper
100
+
101
+ before( :each ) do
102
+ Rake::Task.clear
103
+ @project = cpp_task( :static_library )
104
+ `rm -f #{@project.target}`
105
+ end
106
+
107
+ after( :each ) do
108
+ Rake::Task[ 'clean' ].invoke
109
+ end
110
+
111
+ it 'knows the target type' do
112
+ @project.target_type.should == :static_library
113
+ end
114
+
115
+ it 'builds the library' do
116
+ Rake::Task[ 'build' ].invoke
117
+ exist?( @project.target ).should be_true
118
+ end
119
+
120
+ it 'hasn\'t got a \'run\' task' do
121
+ task_names.include?( 'run' ).should be_false
122
+ end
123
+
124
+ end
125
+
126
+ describe 'when building a shared library' do
127
+
128
+ include RakeBuilderHelper
129
+
130
+ before( :each ) do
131
+ Rake::Task.clear
132
+ @project = cpp_task( :shared_library )
133
+ `rm -f #{ @project.target }`
134
+ end
135
+
136
+ after( :each ) do
137
+ Rake::Task[ 'clean' ].invoke
138
+ end
139
+
140
+ it 'knows the target type' do
141
+ @project.target_type.should == :shared_library
142
+ end
143
+
144
+ it 'builds the library' do
145
+ Rake::Task[ 'build' ].invoke
146
+ exist?( @project.target ).should be_true
147
+ end
148
+
149
+ it 'hasn\'t got a \'run\' task' do
150
+ task_names.include?( 'run' ).should be_false
151
+ end
152
+
153
+ end
154
+
155
+ describe 'when installing' do
156
+
157
+ include RakeBuilderHelper
158
+
159
+ INSTALL_DIRECTORY = '/tmp/rake-builder-test-install'
160
+
161
+ before( :each ) do
162
+ Rake::Task.clear
163
+ `mkdir #{ INSTALL_DIRECTORY }`
164
+ @project = cpp_task( :executable ) do |builder|
165
+ builder.install_path = INSTALL_DIRECTORY
166
+ end
167
+ @installed_target = File.join( INSTALL_DIRECTORY, File.basename( @project.target ) )
168
+ end
169
+
170
+ after( :each ) do
171
+ `rm -rf #{ INSTALL_DIRECTORY }`
172
+ end
173
+
174
+ it 'should install the file' do
175
+ Rake::Task[ 'install' ].invoke
176
+ exist?( @installed_target ).should be_true
177
+ end
178
+
179
+ it 'should uninstall the file' do
180
+ Rake::Task[ 'install' ].invoke
181
+ exist?( @installed_target ).should be_true
182
+ Rake::Task[ 'uninstall' ].invoke
183
+ exist?( @installed_target ).should be_false
184
+ end
185
+
186
+ end