rake-builder 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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