| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with this |
| # work for additional information regarding copyright ownership. The ASF |
| # licenses this file to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| |
| module Buildr #:nodoc: |
| |
| class Options |
| |
| # Runs the build in parallel when true (defaults to false). You can force a parallel build by |
| # setting this option directly, or by running the parallel task ahead of the build task. |
| # |
| # This option only affects recursive tasks. For example: |
| # buildr parallel package |
| # will run all package tasks (from the sub-projects) in parallel, but each sub-project's package |
| # task runs its child tasks (prepare, compile, resources, etc) in sequence. |
| attr_accessor :parallel |
| |
| end |
| |
| task('parallel') { Buildr.options.parallel = true } |
| |
| |
| module Build |
| |
| include Extension |
| |
| first_time do |
| desc 'Build the project' |
| Project.local_task('build') { |name| "Building #{name}" } |
| desc 'Clean files generated during a build' |
| Project.local_task('clean') { |name| "Cleaning #{name}" } |
| |
| desc 'The default task is build' |
| task 'default'=>'build' |
| end |
| |
| before_define(:build => [:compile, :test]) do |project| |
| project.recursive_task 'build' |
| project.recursive_task 'clean' |
| project.clean do |
| rm_rf project.path_to(:target) |
| rm_rf project.path_to(:reports) |
| end |
| end |
| |
| after_define(:build) |
| |
| # :call-seq: |
| # build(*prereqs) => task |
| # build { |task| .. } => task |
| # |
| # Returns the project's build task. With arguments or block, also enhances that task. |
| def build(*prereqs, &block) |
| task('build').enhance prereqs, &block |
| end |
| |
| # :call-seq: |
| # clean(*prereqs) => task |
| # clean { |task| .. } => task |
| # |
| # Returns the project's clean task. With arguments or block, also enhances that task. |
| def clean(*prereqs, &block) |
| task('clean').enhance prereqs, &block |
| end |
| |
| end |
| |
| module Hg #:nodoc: |
| module_function |
| |
| # :call-seq: |
| # hg(*args) |
| # |
| # Executes a Mercurial (hg) command passing through the args and returns the output. |
| # Throws exception if the exit status is not zero. For example: |
| # hg 'commit' |
| # hg 'update', 'default' |
| def hg(*args) |
| cmd = "hg #{args.shift} #{args.map { |arg| arg.inspect }.join(' ')}" |
| output = `#{cmd}` |
| fail "Mercurial command \"#{cmd}\" failed with status #{$?.exitstatus}\n#{output}" unless $?.exitstatus == 0 |
| return output |
| end |
| |
| # Return a list of uncommitted / untracked files as reported by hg status |
| # The codes used to show the status of files are: |
| # M = modified |
| # A = added |
| # R = removed |
| # C = clean |
| # ! = missing (deleted by non-hg command, but still tracked) |
| # ? = not tracked |
| # I = ignored |
| # = origin of the previous file listed as A (added) |
| def uncommitted_files |
| `hg status`.scan(/^(A|M|R|!|\?) (\S.*)$/).map{ |match| match.last.split.last } |
| end |
| |
| # Commit the given file with a message. The file should already be added to the Mercurial index. |
| def commit(file, message) |
| hg 'commit', '-m', message, file |
| end |
| |
| # Update the remote branch with the local commits |
| # This will push the current remote destination and current branch. |
| def push |
| hg 'push' |
| end |
| |
| # Return the name of the current local branch or nil if none. |
| def current_branch |
| hg('branch').to_s.strip |
| end |
| |
| # Return the aliases (if any) of any remote repositories which the current local branch tracks |
| def remote |
| hg('paths').scan(/^(?:default|default-push)\s+=\s+(\S.*)/).map{ |match| match.last } |
| end |
| end |
| |
| module Git #:nodoc: |
| module_function |
| |
| # :call-seq: |
| # git(*args) |
| # |
| # Executes a Git command and returns the output. Throws exception if the exit status |
| # is not zero. For example: |
| # git 'commit' |
| # git 'remote', 'show', 'origin' |
| def git(*args) |
| cmd = "git #{args.shift} #{args.map { |arg| arg.inspect }.join(' ')}" |
| output = `#{cmd}` |
| fail "GIT command \"#{cmd}\" failed with status #{$?.exitstatus}\n#{output}" unless $?.exitstatus == 0 |
| return output |
| end |
| |
| # Returns list of uncommited/untracked files as reported by git status. |
| def uncommitted_files |
| `git status`.scan(/^#(\t|\s{7})(\S.*)$/).map { |match| match.last.split.last } |
| end |
| |
| # Commit the given file with a message. |
| # The file has to be known to Git meaning that it has either to have been already committed in the past |
| # or freshly added to the index. Otherwise it will fail. |
| def commit(file, message) |
| git 'commit', '-m', message, file |
| end |
| |
| # Update the remote refs using local refs |
| # |
| # By default, the "remote" destination of the push is the the remote repo linked to the current branch. |
| # The default remote branch is the current local branch. |
| def push(remote_repo = remote, remote_branch = current_branch) |
| git 'push', remote, current_branch |
| end |
| |
| # Return the name of the remote repository whose branch the current local branch tracks, |
| # or nil if none. |
| def remote(branch = current_branch) |
| remote = git('config', '--get', "branch.#{branch}.remote").to_s.strip |
| remote if !remote.empty? && git('remote').include?(remote) |
| end |
| |
| # Return the name of the current branch |
| def current_branch |
| git('branch')[/^\* (.*)$/, 1] |
| end |
| end |
| |
| |
| module Svn #:nodoc: |
| module_function |
| |
| # :call-seq: |
| # svn(*args) |
| # |
| # Executes a SVN command and returns the output. Throws exception if the exit status |
| # is not zero. For example: |
| # svn 'commit' |
| def svn(*args) |
| output = `svn #{args.shift} #{args.map { |arg| arg.inspect }.join(' ')}` |
| fail "SVN command failed with status #{$?.exitstatus}" unless $?.exitstatus == 0 |
| return output |
| end |
| |
| def tag(tag_name) |
| url = tag_url repo_url, tag_name |
| remove url, 'Removing old copy' rescue nil |
| copy Dir.pwd, url, "Release #{tag_name}" |
| end |
| |
| # Status check reveals modified files, but also SVN externals which we can safely ignore. |
| def uncommitted_files |
| svn('status', '--ignore-externals').split("\n").reject { |line| line =~ /^X\s/ } |
| end |
| |
| def commit(file, message) |
| svn 'commit', '-m', message, file |
| end |
| |
| # :call-seq: |
| # tag_url(svn_url, version) => tag_url |
| # |
| # Returns the SVN url for the tag. |
| # Can tag from the trunk or from branches. |
| # Can handle the two standard repository layouts. |
| # - http://my.repo/foo/trunk => http://my.repo/foo/tags/1.0.0 |
| # - http://my.repo/trunk/foo => http://my.repo/tags/foo/1.0.0 |
| def tag_url(svn_url, tag) |
| trunk_or_branches = Regexp.union(%r{^(.*)/trunk(.*)$}, %r{^(.*)/branches(.*)/([^/]*)$}) |
| match = trunk_or_branches.match(svn_url) |
| prefix = match[1] || match[3] |
| suffix = match[2] || match[4] |
| prefix + '/tags' + suffix + '/' + tag |
| end |
| |
| # Return the current SVN URL |
| def repo_url |
| svn('info', '--xml')[/<url>(.*?)<\/url>/, 1].strip |
| end |
| |
| def copy(dir, url, message) |
| svn 'copy', '--parents', dir, url, '-m', message |
| end |
| |
| def remove(url, message) |
| svn 'remove', url, '-m', message |
| end |
| |
| end |
| |
| |
| class Release #:nodoc: |
| |
| THIS_VERSION_PATTERN = /(THIS_VERSION|VERSION_NUMBER)\s*=\s*(["'])(.*)\2/ |
| |
| class << self |
| |
| # Use this to specify a different tag name for tagging the release in source control. |
| # You can set the tag name or a proc that will be called with the version number, |
| # for example: |
| # Release.tag_name = lambda { |ver| "foo-#{ver}" } |
| attr_accessor :tag_name |
| |
| # Use this to specify a different commit message to commit the buildfile with the next version in source control. |
| # You can set the commit message or a proc that will be called with the next version number, |
| # for example: |
| # Release.commit_message = lambda { |ver| "Changed version number to #{ver}" } |
| attr_accessor :commit_message |
| |
| # Use this to specify the next version number to replace VERSION_NUMBER with in the buildfile. |
| # You can set the next version or a proc that will be called with the current version number. |
| # For example, with the following buildfile: |
| # THIS_VERSION = "1.0.0-rc1" |
| # Release.next_version = lambda { |version| |
| # version[-1] = version[-1].to_i + 1 |
| # version |
| # } |
| # |
| # Release.next_version will return "1.0.0-rc2", so at the end of the release, the buildfile will contain VERSION_NUMBER = "1.0.0-rc2" |
| # |
| attr_accessor :next_version |
| |
| # :call-seq: |
| # add(MyReleaseClass) |
| # |
| # Add a Release implementation to the list of available Release classes. |
| def add(release) |
| @list ||= [] |
| @list |= [release] |
| end |
| alias :<< :add |
| |
| # The list of supported Release implementations |
| def list |
| @list ||= [] |
| end |
| |
| # Finds and returns the Release instance for this project. |
| def find |
| unless @release |
| klass = list.detect { |impl| impl.applies_to? } |
| @release = klass.new if klass |
| end |
| @release |
| end |
| |
| end |
| |
| # :call-seq: |
| # make() |
| # |
| # Make a release. |
| def make |
| @this_version = extract_version |
| check |
| with_release_candidate_version do |release_candidate_buildfile| |
| args = [] |
| args << 'buildr' << '--buildfile' << release_candidate_buildfile |
| args << '--environment' << Buildr.environment unless Buildr.environment.to_s.empty? |
| args << 'clean' << 'upload' << 'DEBUG=no' |
| sh *args |
| end |
| tag_release resolve_tag |
| update_version_to_next if this_version != resolve_next_version(this_version) |
| end |
| |
| def check |
| if this_version == resolve_next_version(this_version) && this_version.match(/-SNAPSHOT$/) |
| fail "The next version can't be equal to the current version #{this_version}.\nUpdate THIS_VERSION/VERSION_NUMBER, specify Release.next_version or use NEXT_VERSION env var" |
| end |
| end |
| |
| # :call-seq: |
| # extract_version() => this_version |
| # |
| # Extract the current version number from the buildfile. |
| # Raise an error if not found. |
| def extract_version |
| buildfile = File.read(version_file) |
| buildfile.scan(THIS_VERSION_PATTERN)[0][2] |
| rescue |
| fail 'Looking for THIS_VERSION = "..." in your Buildfile, none found' |
| end |
| |
| # Use this to specify a different tag name for tagging the release in source control. |
| # You can set the tag name or a proc that will be called with the version number, |
| # for example: |
| # Release.find.tag_name = lambda { |ver| "foo-#{ver}" } |
| # Deprecated: you should use Release.tag_name instead |
| def tag_name=(tag_proc) |
| Buildr.application.deprecated "Release.find.tag_name is deprecated. You should use Release.tag_name instead" |
| Release.tag_name=(tag_proc) |
| end |
| |
| protected |
| |
| # the initial value of THIS_VERSION |
| attr_accessor :this_version |
| |
| # :call-seq: |
| # version_file() |
| # Provides the file containing the version of the project. |
| # If the project contains a version.rb file next to the Buildr build file, |
| # it is used. Otherwise, always use the buildfile. |
| def version_file |
| version_rb_file = File.dirname(Buildr.application.buildfile.to_s) + '/version.rb' |
| return version_rb_file if File.exists?(version_rb_file) |
| return Buildr.application.buildfile.to_s |
| end |
| |
| # :call-seq: |
| # with_release_candidate_version() { |filename| ... } |
| # |
| # Yields to block with release candidate buildfile, before committing to use it. |
| # |
| # We need a Buildfile with upgraded version numbers to run the build, but we don't want the |
| # Buildfile modified unless the build succeeds. So this method updates the version number in |
| # a separate (Buildfile.next) file, yields to the block with that filename, and if successful |
| # copies the new file over the existing one. |
| # |
| # The release version is the current version without '-SNAPSHOT'. So: |
| # THIS_VERSION = 1.1.0-SNAPSHOT |
| # becomes: |
| # THIS_VERSION = 1.1.0 |
| # for the release buildfile. |
| def with_release_candidate_version |
| release_candidate_buildfile = version_file + '.next' |
| |
| release_candidate_buildfile_contents = change_version { |version| |
| version.gsub(/-SNAPSHOT$/, "") |
| } |
| File.open(release_candidate_buildfile, 'w') { |file| file.write release_candidate_buildfile_contents } |
| begin |
| yield release_candidate_buildfile |
| mv release_candidate_buildfile, version_file |
| ensure |
| rm release_candidate_buildfile rescue nil |
| end |
| end |
| |
| # :call-seq: |
| # change_version() { |this_version| ... } => buildfile |
| # |
| # Change version number in the current Buildfile, but without writing a new file (yet). |
| # Returns the contents of the Buildfile with the modified version number. |
| # |
| # This method yields to the block with the current (this) version number and expects |
| # the block to return the updated version. |
| def change_version |
| current_version = extract_version |
| new_version = yield(current_version) |
| buildfile = File.read(version_file) |
| buildfile.gsub(THIS_VERSION_PATTERN) { |ver| ver.sub(/(["']).*\1/, %Q{"#{new_version}"}) } |
| end |
| |
| # Return the name of the tag to tag the release with. |
| def resolve_tag |
| version = extract_version |
| tag = Release.tag_name || version |
| tag = tag.call(version) if Proc === tag |
| tag |
| end |
| |
| # Return the new value of THIS_VERSION based on the version passed. |
| # |
| # This method receives the existing value of THIS_VERSION |
| def resolve_next_version(current_version) |
| next_version = Release.next_version |
| next_version ||= lambda { |v| |
| snapshot = v.match(/-SNAPSHOT$/) |
| version = v.gsub(/-SNAPSHOT$/, "").split(/\./) |
| if snapshot |
| version[-1] = sprintf("%0#{version[-1].size}d", version[-1].to_i + 1) + '-SNAPSHOT' |
| end |
| version.join('.') |
| } |
| next_version = ENV['NEXT_VERSION'] if ENV['NEXT_VERSION'] |
| next_version = ENV['next_version'] if ENV['next_version'] |
| next_version = next_version.call(current_version) if Proc === next_version |
| next_version |
| end |
| |
| # Move the version to next and save the updated buildfile |
| def update_buildfile |
| buildfile = change_version { |version| # THIS_VERSION minus SNAPSHOT |
| resolve_next_version(this_version) # THIS_VERSION |
| } |
| File.open(version_file, 'w') { |file| file.write buildfile } |
| end |
| |
| # Return the message to use to commit the buildfile with the next version |
| def message |
| version = extract_version |
| msg = Release.commit_message || "Changed version number to #{version}" |
| msg = msg.call(version) if Proc === msg |
| msg |
| end |
| |
| def update_version_to_next |
| update_buildfile |
| end |
| end |
| |
| |
| class HgRelease < Release |
| class << self |
| def applies_to? |
| if File.exist? '.hg/requires' |
| true |
| else |
| curr_pwd = Dir.pwd |
| Dir.chdir('..') do |
| return false if curr_pwd == Dir.pwd # Means going up one level is not possible. |
| applies_to? |
| end |
| end |
| end |
| end |
| |
| # Fails if one of these 2 conditions are not met: |
| # 1. The reository is not 'clean'; no content staged or unstaged |
| # 2. The repository is only a local repository and has no remote refs |
| def check |
| super |
| info "Working in branch '#{Hg.current_branch}'" |
| uncommitted = Hg.uncommitted_files |
| fail "Uncommitted files violate the First Principle Of Release!\n#{uncommitted.join("\n")}" unless uncommitted.empty? |
| fail "You are releasing from a local branch that does not track a remote!" if Hg.remote.empty? |
| end |
| |
| # Tag this release in Mercurial |
| def tag_release(tag) |
| unless this_version == extract_version |
| info "Committing buildfile with version number #{extract_version}" |
| Hg.commit File.basename(version_file), message |
| Hg.push if Hg.remote |
| end |
| info "Tagging release #{tag}" |
| Hg.hg 'tag', tag, '-m', "[buildr] Cutting release #{tag}" |
| Hg.push if Hg.remote |
| end |
| |
| # Update buildfile with next version number |
| def update_version_to_next |
| super |
| info "Current version is now #{extract_version}" |
| Hg.commit File.basename(version_file), message |
| Hg.push if Hg.remote |
| end |
| end |
| |
| |
| class GitRelease < Release |
| class << self |
| def applies_to? |
| if File.exist? '.git/config' |
| true |
| else |
| curr_pwd = Dir.pwd |
| Dir.chdir('..') do |
| return false if curr_pwd == Dir.pwd # Means going up one level is not possible. |
| applies_to? |
| end |
| end |
| end |
| end |
| |
| # Fails if one of these 2 conditions are not met: |
| # 1. the repository is clean: no content staged or unstaged |
| # 2. some remote repositories are defined but the current branch does not track any |
| def check |
| super |
| uncommitted = Git.uncommitted_files |
| fail "Uncommitted files violate the First Principle Of Release!\n#{uncommitted.join("\n")}" unless uncommitted.empty? |
| fail "You are releasing from a local branch that does not track a remote!" unless Git.remote |
| end |
| |
| # Add a tag reference in .git/refs/tags and push it to the remote if any. |
| # If a tag with the same name already exists it will get deleted (in both local and remote repositories). |
| def tag_release(tag) |
| unless this_version == extract_version |
| info "Committing buildfile with version number #{extract_version}" |
| Git.commit File.basename(version_file), message |
| Git.push if Git.remote |
| end |
| info "Tagging release #{tag}" |
| Git.git 'tag', '-d', tag rescue nil |
| Git.git 'push', Git.remote, ":refs/tags/#{tag}" rescue nil if Git.remote |
| Git.git 'tag', '-a', tag, '-m', "[buildr] Cutting release #{tag}" |
| Git.git 'push', Git.remote, 'tag', tag if Git.remote |
| end |
| |
| def update_version_to_next |
| super |
| info "Current version is now #{extract_version}" |
| Git.commit File.basename(version_file), message |
| Git.push if Git.remote |
| end |
| end |
| |
| |
| class SvnRelease < Release |
| class << self |
| def applies_to? |
| File.exist?('.svn') |
| end |
| end |
| |
| def check |
| super |
| fail "Uncommitted files violate the First Principle Of Release!\n"+Svn.uncommitted_files.join("\n") unless Svn.uncommitted_files.empty? |
| fail "SVN URL must contain 'trunk' or 'branches/...'" unless Svn.repo_url =~ /(trunk)|(branches.*)$/ |
| end |
| |
| def tag_release(tag) |
| # Unlike Git, committing the buildfile with the released version is not necessary. |
| # svn tag does commit & tag. |
| info "Tagging release #{tag}" |
| Svn.tag tag |
| end |
| |
| def update_version_to_next |
| super |
| info "Current version is now #{extract_version}" |
| Svn.commit version_file, message |
| end |
| end |
| |
| Release.add HgRelease |
| Release.add SvnRelease |
| Release.add GitRelease |
| |
| desc 'Make a release' |
| task 'release' do |task| |
| release = Release.find |
| fail 'Unable to detect the Version Control System.' unless release |
| release.make |
| end |
| |
| end |
| |
| class Buildr::Project #:nodoc: |
| include Buildr::Build |
| end |