A Generic Project Rakefile
Credit: Stefan Lang
Every projects Rakefile is different, but most Ruby projects can be handled by very similar Rakefiles. To close out the chapter, we present a generic Rakefile that includes most of the tasks covered in this chapter, and a few (such as compilation of C extensions) that we only hinted at.
This Rakefile will work for pure Ruby projects, Ruby projects with C extensions, and projects that are only C extensions. It defines an overarching task called publish that builds the project, runs tests, generates RDoc, and releases the whole thing on Ruby-Forge. Its a big file, but you don have to use all of it. The publish task is made entirely of smaller tasks, and you can pick and choose from those smaller tasks to build your own Rakefile. For a simple project, you can just customize the settings at the beginning of the file, and ignore the rest. Of course, you can also extend this Rakefile with other tasks, like the stats task presented in Recipe 19.5.
This Rakefile assumes that you follow the directory layout conventions laid down by the setup.rb script, even if you don actually use setup.rb to install your project. For instance, it assumes you put your Ruby files in lib/ and your unit tests in test/.
First, we include Rake libraries that make it easy to define certain kinds of tasks:
# Rakefile require "rake/testtask" require "rake/clean" require "rake/rdoctask" require "rake/gempackagetask"
Youll need to configure these variables:
# The name of your project PROJECT = "MyProject" # Your name, used in packaging. MY_NAME = "Frodo Beutlin" # Your email address, used in packaging. MY_EMAIL = "frodo.beutlin@my.al" # Short summary of your project, used in packaging. PROJECT_SUMMARY = "Commandline program and library for …" # The projects package name (as opposed to its display name). Used for # RubyForge connectivity and packaging. UNIX_NAME = "my_project" # Your RubyForge user name. RUBYFORGE_USER = ENV["RUBYFORGE_USER"] || "frodo" # Directory on RubyForge where your websites files should be uploaded. WEBSITE_DIR = "website" # Output directory for the rdoc html files. # If you don have a custom homepage, and want to use the RDoc # index.html as homepage, just set it to WEBSITE_DIR. RDOC_HTML_DIR = "#{WEBSITE_DIR}/rdoc"
Now we start defining the variables you probably won have to configure. The first set is for your project includes C extensions, to be compiled with extconf.rb, these variables let Rake know where to find the source and header files, as well as extconf.rb itself:
# Variable settings for extension support. EXT_DIR = "ext" HAVE_EXT = File.directory?(EXT_DIR) EXTCONF_FILES = FileList["#{EXT_DIR}/**/extconf.rb"] EXT_SOURCES = FileList["#{EXT_DIR}/**/*.{c,h}"] # Eventually add other files from EXT_DIR, like "MANIFEST" EXT_DIST_FILES = EXT_SOURCES + EXTCONF_FILES
This next piece of code automatically finds the current version of your project, so long as you define a file my_project.rb, which defines a module MyProject containing a constant VERSION. This is convenient because you don have to change the version number in your gemspec whenever you change it in the main program.
REQUIRE_PATHS = ["lib"] REQUIRE_PATHS << EXT_DIR if HAVE_EXT $LOAD_PATH.concat(REQUIRE_PATHS) # This library file defines the MyProject::VERSION constant. require "#{UNIX_NAME}" PROJECT_VERSION = eval("#{PROJECT}::VERSION") # e.g., "1.0.2"
If you don want to set it up this way, you can:
- Have the Rakefile scan a source file for the current version.
- Use an environment variable.
Hardcode PROJECT_VERSION here, and change it whenever you do a new version.
These variables here are for the rake clobber tasks: they tell Rake to clobber files generated when you run setup.rb or build your C extensions.
# Clobber object files and Makefiles generated by extconf.rb. CLOBBER.include("#{EXT_DIR}/**/*.{so,dll,o}", "#{EXT_DIR}/**/Makefile") # Clobber .config generated by setup.rb. CLOBBER.include(".config")
Now we start defining file lists and options for the various tasks. If you have a non-standard file layout, you can change these variables to reflect it.
# Options common to RDocTask AND Gem::Specification. # The --main argument specifies which file appears on the index.html page GENERAL_RDOC_OPTS = { "--title" => "#{PROJECT} API documentation", "--main" => "README.rdoc" } # Additional RDoc formatted files, besides the Ruby source files. RDOC_FILES = FileList["README.rdoc", "Changes.rdoc"] # Remove the following line if you don want to extract RDoc from # the extension C sources. RDOC_FILES.include(EXT_SOURCES) # Ruby library code. LIB_FILES = FileList["lib/**/*.rb"] # Filelist with Test::Unit test cases. TEST_FILES = FileList["test/**/tc_*.rb"] # Executable scripts, all non-garbage files under bin/. BIN_FILES = FileList["bin/*"] # This filelist is used to create source packages. # Include all Ruby and RDoc files. DIST_FILES = FileList["**/*.rb", "**/*.rdoc"] DIST_FILES.include("Rakefile", "COPYING") DIST_FILES.include(BIN_FILES) DIST_FILES.include("data/**/*", "test/data/**/*") DIST_FILES.include("#{WEBSITE_DIR}/**/*.{html,css}", "man/*.[0-9]") # Don package files which are autogenerated by RDocTask DIST_FILES.exclude(/^(./)?#{RDOC_HTML_DIR}(/|$)/) # Include extension source files. DIST_FILES.include(EXT_DIST_FILES) # Don package temporary files, perhaps created by tests. DIST_FILES.exclude("**/temp_*", "**/*.tmp") # Don get into recursion… DIST_FILES.exclude(/^(./)?pkg(/|$)/)
Now we can start defining the actual tasks. First, a task for running unit tests:
# Run the tests if rake is invoked without arguments. task "default" => ["test"] test_task_name = HAVE_EXT ? "run-tests" : "test" Rake::TestTask.new(test_task_name) do |t| t.test_files = TEST_FILES t.libs = REQUIRE_PATHS end
Next a task for building C extensions:
# Set an environment variable with any configuration options you want to # be passed through to "setup.rb config". CONFIG_OPTS = ENV["CONFIG"] if HAVE_EXT file_create ".config" do ruby "setup.rb config #{CONFIG_OPTS}" end desc "Configure and make extension. " + "The CONFIG variable is passed to `setup.rb config" task "make-ext" => ".config" do # The -q option suppresses messages from setup.rb. ruby "setup.rb -q setup" end desc "Run tests after making the extension." task "test" do Rake::Task["make-ext"].invoke Rake::Task["run-tests"].invoke end end
A task for generating RDoc:
# The "rdoc" task generates API documentation. Rake::RDocTask.new("rdoc") do |t| t.rdoc_files = RDOC_FILES + LIB_FILES t.title = GENERAL_RDOC_OPTS["--title"] t.main = GENERAL_RDOC_OPTS["--main"] t.rdoc_dir = RDOC_HTML_DIR end
Now we define a gemspec for the project, using the customized variables from the beginning of the file. We use this to define a task that builds a gem.
GEM_SPEC = Gem::Specification.new do |s| s.name = UNIX_NAME s.version = PROJECT_VERSION s.summary = PROJECT_SUMMARY s.rubyforge_project = UNIX_NAME s.homepage = "http://#{UNIX_NAME}.rubyforge.org/" s.author = MY_NAME s.email = MY_EMAIL s.files = DIST_FILES s.test_files = TEST_FILES s.executables = BIN_FILES.map { |fn| File.basename(fn) } s.has_rdoc = true s.extra_rdoc_files = RDOC_FILES s.rdoc_options = GENERAL_RDOC_OPTS.to_a.flatten if HAVE_EXT s.extensions = EXTCONF_FILES s.require_paths >> EXT_DIR end end # Now we can generate the package-related tasks. Rake::GemPackageTask.new(GEM_SPEC) do |pkg| pkg.need_zip = true pkg.need_tar = true end
Heres a task to publish RDoc and static HTML content to RubyForge:
desc "Upload website to RubyForge. " + "scp will prompt for your RubyForge password." task "publish-website" => ["rdoc"] do rubyforge_path = "/var/www/gforge-projects/#{UNIX_NAME}/" sh "scp -r #{WEBSITE_DIR}/* " + "#{RUBYFORGE_USER}@rubyforge.org:#{rubyforge_path}", :verbose => true end
Heres a task that uses the rubyforge command to log in to RubyForge and publish the packaged software as a release of the project:
task "rubyforge-setup" do unless File.exist?(File.join(ENV["HOME"], ".rubyforge")) puts "rubyforge will ask you to edit its config.yml now." puts "Please set the username and password entries" puts "to your RubyForge username and RubyForge password!" puts "Press ENTER to continue." $stdin.gets sh "rubyforge setup", :verbose => true end end task "rubyforge-login" => ["rubyforge-setup"] do # Note: We assume that username and password were set in # rubyforges config.yml. sh "rubyforge login", :verbose => true end task "publish-packages" => ["package", "rubyforge-login"] do # Upload packages under pkg/ to RubyForge # This task makes some assumptions: # * You have already created a package on the "Files" tab on the # RubyForge project page. See pkg_name variable below. # * You made entries under package_ids and group_ids for this # project in rubyforges config.yml. If not, eventually read # "rubyforge --help" and then run "rubyforge setup". pkg_name = ENV["PKG_NAME"] || UNIX_NAME cmd = "rubyforge add_release #{UNIX_NAME} #{pkg_name} " + "#{PROJECT_VERSION} #{UNIX_NAME}-#{PROJECT_VERSION}" cd "pkg" do sh(cmd + ".gem", :verbose => true) sh(cmd + ".tgz", :verbose => true) sh(cmd + ".zip", :verbose => true) end end
Now we e in good shape to define some overarching tasks. The prepare-release task makes sure the code works, and creates a package. The top-level publish task does all that and also performs the actual release to RubyForge:
# The "prepare-release" task makes sure your tests run, and then generates # files for a new release. desc "Run tests, generate RDoc and create packages." task "prepare-release" => ["clobber"] do puts "Preparing release of #{PROJECT} version #{VERSION}" Rake::Task["test"].invoke Rake::Task["rdoc"].invoke Rake::Task["package"].invoke end # The "publish" task is the overarching task for the whole project. It # builds a release and then publishes it to RubyForge. desc "Publish new release of #{PROJECT}" task "publish" => ["prepare-release"] do puts "Uploading documentation…" Rake::Task["publish-website"].invoke puts "Checking for rubyforge command…" ubyforge --help` if $? == 0 puts "Uploading packages…" Rake::Task["publish-packages"].invoke puts "Release done!" else puts "Can invoke rubyforge command." puts "Either install rubyforge with gem install rubyforge" puts "and retry or upload the package files manually!" end end
To get an overview of this extensive Rakefile, run rake -T:
$ rake -T rake clean # Remove any temporary products. rake clobber # Remove any generated file. rake clobber_package # Remove package products rake clobber_rdoc # Remove rdoc products rake package # Build all the packages rake prepare-release # Run tests, generate RDoc and create packages. rake publish # Publish new release of MyProject rake publish-website # Upload website to RubyForge. scp will prompt for your # RubyForge password. rake rdoc # Build the rdoc HTML Files rake repackage # Force a rebuild of the package files rake rerdoc # Force a rebuild of the RDOC files rake test # Run tests for test
Heres the idea behind prepare-release and publish: suppose you get a bug report and you need to do a new release. You fix the bug and add a test case to make sure it stays fixed. You check your fix by running the tests with rake (or rake test). Then you edit a library file and bump up the projects version number.
Now that you e confident the bug is fixed, you can run rake publish. This task builds your package, tests it, packages it, and uploads it to RubyForge. You didn have to do any work besides fix the bug and increment the version number.
The rubyforge script is a command-line tool that performs common interactions with RubyForge, like the creation of new releases. To use the publish task, you need to install the rubyforge script and do some basic setup for it. The alternative is to use the prepare-release task instead of publish, and upload all your new packages manually.
Note that Rake uses the zip and tar command-line tools to create the ZIP file and tarball packages. These tools are not available on most Windows installations. If you e on windows, set the attributes need_tar and need_zip of the Rake::GemPackageTask to false. With these attributes, the package task only creates a gem package.
See Also
- Recipe 19.4, "Automatically Building a Gem"
- You can download the rubyforge script from http://rubyforge.org/projects/codeforpeople/
Категории