= %s):" %
[@project, @d_ver, @status]
puts "Project page: %s" % release.project.url
puts "Release: %s (%s)" % [release.version, release.sym_status]
puts "Download URL: %s)" % release.url
raise SkipRequested
end
@release.save_file
@filename = release.dest_file
end
private
def fetch_info(status)
Logger.instance.debug(("Preparing package for '%s' for Drupal %s, "+
"status >= %s") % [project, d_ver, status])
@release = DrupalProject::VersionsList.for(@project).
choose(d_ver, status)
Logger.instance.info "Found %s version %s (status: %s)" %
[@release.project, @release.version, @release.sym_status]
Logger.instance.debug "Download URL: #{release.url}"
end
end
class DebianPackager
def initialize(down)
if ! Options.debianize
Logger.instance.debug('Skipping Debian package creation as ' +
'requested at command line')
raise SkipRequested
end
Logger.instance.debug 'Starting Debian package creation'
@d_ver = down.d_ver
@release = down.release
@project = @release.project
@version = @release.version
@author = @project.author
@maint_name = ENV['DEBFULLNAME'] || Etc::getpwuid.gecos.gsub(/,+$/, '')
@maint_mail = ENV['DEBEMAIL'] || ENV['EMAIL']
@pkgname = PackageName.for(@project.name, @project.p_type, @d_ver)
@tarball = down.filename
@instdir = '%s-%s' % [@pkgname, @release.version]
@builddir = '/usr/share/drupal%s' % @d_ver
if @project.p_type.ck_tar_in_dir?
@builddir = File.join(@builddir, @project.p_type.dir, @project.name )
end
ck_orig_tarball
Logger.instance.info 'Debian package name: %s' % @pkgname
end
# Unpacks the downloaded tarball and creates the Debian package
# structure in it
def build_structure
setup_directory
setup_source_format
setup_changelog
setup_compat
setup_control
setup_copyright
setup_dirs
setup_install_files
setup_watch
setup_rules
end
# Builds the Debian package from the created directory structure
def build_package(switches)
if Options.skip_build
Logger.instance.debug('Skipping Debian package build as requested at ' +
'command line')
raise SkipRequested
end
if ! File.exists? '/usr/bin/dpkg-buildpackage'
Logger.instance.error 'dpkg-buildpackage not found - Cannot build ' +
'the generated package.'
exit 1
end
cmdline = '/usr/bin/dpkg-buildpackage %s' % switches
Logger.instance.info 'Starting Debian package build'
Logger.instance.debug 'Invoking external command: %s' % cmdline
system('cd %s && %s 2>&1' % [@instdir, cmdline])
end
private
# Checks if the original tarball looks like a sane Drupal project
# file, and populate @filelist with the relevant
# information. Raises a RuntimeError if it does not look right.
def ck_orig_tarball
@filelist = []
@filelist = IO.popen('tar tzf %s' % @tarball).readlines.map do |file|
if @project.p_type.ck_tar_in_dir?
# Project types which ship their whole contents inside a
# directory with the same name as themselves (modules,
# themes): Refuse to continue if there are files I don't know
# how to handle (i.e. are not in the expected place)
raise RuntimeError,('Downloaded file %s has an unexpected '+
'directory hierarchy (%s) - Aborting.') %
[@tarball, file] unless file.gsub!(/^#{@project.name}\//, '')
end
file.gsub(/\n$/, '')
end.reject {|file| file.empty?}
Logger.instance.debug('Original tarball verified - %s files included' %
@filelist.size)
end
# Sets up the directory for starting the Debian packaging
def setup_directory
curdir = Dir.pwd
FileUtils.rm_r(@instdir) if Options.force_overwrite and
File.exists?(@instdir)
Dir.mkdir(@instdir)
Dir.mkdir(File.join(@instdir, 'debian'))
Dir.mktmpdir do |tmpdir|
system("cd #{tmpdir}; tar xzf #{File.join(curdir, @tarball)}")
move_from = (@project.p_type.ck_tar_in_dir? ?
File.join(tmpdir, @project.name) : tmpdir)
Dir.open(move_from).entries.reject { |e|
['.','..'].include? e
}.each {|f|
FileUtils.mv(File.join(move_from,f), @instdir)
}
end
end
# Sets up the source format declaration
def setup_source_format
Dir.mkdir(File.join(@instdir, 'debian/source'))
put_in_file 'source/format', '3.0 (quilt)'
end
# Creates the debian/changelog file
def setup_changelog
timestamp = Time.now.strftime '%a, %d %b %Y %H:%M:%S %z'
deb_ver = '%s-1' % @release.version
distr = 'unstable'
entry = ("%s (%s) %s; urgency=low\n\n" % [@pkgname, deb_ver, distr]) +
" * Initial release\n\n" +
(" -- %s <%s> %s" % [@maint_name, @maint_mail, timestamp] )
put_in_file 'changelog', entry
end
# Creates the debian/compat file
def setup_compat
put_in_file 'compat', '8'
end
# Creates the debian/watch file
def setup_watch
res = ['version=3',
( 'https://drupal.org/project/%s .*/%s-%s.x-(\d[\d_.]+)\.tar\.gz' %
[@project.name, @project.name, @d_ver] ) ].join("\n")
put_in_file 'watch', res
end
# Creates the debian/copyright file
def setup_copyright
res = ['Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/',
'Upstream-Name: %s' % @project.name,
'Source: %s' % @project.url,
'Warning:',
' -=-=-=- WARNING -=-=-=-',
' This file has been autogenerated by dh-make-drupal.',
' .',
' While this program does its best to achieve proper results,',
' copyright information is a very sensible topic which REQUIRES',
' HUMAN VALIDATION. Please make sure that this information is',
' correct.',
' -=-=-=-=-=-=-=-=-=-=-=-',
'',
'Files: *'
]
if @project.author
start_yr = @project.creation.year
release_yr = @release.date.year
years = (start_yr == release_yr) ? start_yr :
'%s - %s' % [start_yr, release_yr]
res << 'Copyright: %s %s (%s)' %
[ years, @project.author.name, @project.author.info_url ]
Logger.instance.debug "Author copyright information found: \n" +
res.join("\n")
else
Logger.instance.debug 'Author copyright information not found'
res << 'Copyright: Copyright information could not be found'
end
# Canonically, Drupal modules include LICENSE.txt. Even more,
# canonically it is the GPLv2 - For further joy, it's usually
# one of two exact same files! :-)
lic_file = find_license[0]
if (! lic_file.nil? and license = File.join(@instdir, lic_file))
data = File.read(license) || '' # Avoid an exception if file is missing
if ["998ed0c116c0cebfcd9b2107b0d82973",
"b234ee4d69f5fce4486a80fdaf4a4263"].include? Digest::MD5.hexdigest(data)
res << 'License: GPL-2' << '' << 'License: GPL-2' <<
' This package is licensed under the GNU General Public ' <<
' License (GPL) version 2.' << ' .' <<
' On Debian GNU/Linux systems, the complete text of the GNU ' <<
' General Public License can be found in:' << ' .' <<
' /usr/share/common-licenses/GPL-2'
Logger.instance.debug 'Upstream ships the canonical GPLv2'
else
res << 'License: Unknown' << '' << 'License: Unknown' <<
'Cannot automatically determine the license. Please check by hand.' <<
' .' << ' Author-supplied data:' << ' .' << data
Logger.instance.info('Cannot automatically determine the chosen ' +
'license - Please check by hand')
end
else
Logger.instance.warn('No license file found in distribution, cannot ' +
'guess copyright information - Please check by ' +
'hand.')
res << 'License: Unknown' << '' << 'License: Unknown' <<
' Copyright information could not be found in the sources — Please' <<
' check by hand'
end
put_in_file 'copyright', res.join("\n")
end
# Creates the debian/control file
def setup_control
Options.provides ||= []
depends = Dependencies.new(@release, @instdir).get - Options.provides
# For the recommendations, we remove anything that would be
# duplicated from the dependencies
recommends = ( Options.skip_recommend ? [] :
( Recommendations.new(@release, @instdir).get -
(Options.provides + depends) ))
long = long_descr
control = ['Source: %s' % @pkgname,
'Section: web',
'Priority: extra',
'Maintainer: %s <%s>' % [@maint_name, @maint_mail],
'Build-Depends: debhelper (>= 8.0.0)',
'Standards-Version: 3.9.6',
'Homepage: %s' % @project.url,
'',
'Package: %s' % @pkgname,
'Architecture: all',
'Depends: %s' % depends.join(', ')]
if !recommends.empty?
control << 'Recommends: %s' % recommends.join(', ')
long += ("\n\nThe 'recommended' packages were auto-generated " +
"by dh-make-drupal out of several naïve assumptions " +
"and might not be real packages. ").word_wrap.prefix
end
if !Options.provides.empty?
control << 'Provides: %s' % Options.provides.map {|mod|
PackageName.for(mod, ProjType.new('Modules'), @d_ver)
}.join(', ')
end
control << 'Description: %s' % short_descr
control << long
put_in_file 'control', control.join("\n")
end
# Creates the debian/rules file
def setup_rules
put_in_file 'rules', ['#!/usr/bin/make -f', '%:', "\tdh $@"].join("\n")
FileUtils.chmod(0755, File.join(@instdir, 'debian', 'rules'))
end
# Creates the debian/dirs file
def setup_dirs
subdirs = subdirs_for(@instdir).reject {|d| d =~ /^#{@instdir}.debian/}
dirs = [@builddir, subdirs.map {|d| File.join(@builddir,d)}].flatten
put_in_file '%s.dirs' % @pkgname, dirs.join("\n")
end
def setup_install_files
install = files_to_install.map {|f| '%s %s' % [f, @builddir]}
docs = find_docs
changelogs = find_changelog
['changelogs','docs','install'].each do |f|
eval('put_in_file "%s.%s", %s.sort.join("\n") unless %s.empty? ' %
[@pkgname,f,f,f])
end
end
# Builds a short description for the package
def short_descr
'%s %s for Drupal %s' % [@project.name, @project.p_type.human.downcase,
@d_ver]
end
# Gets the long description for the package
def long_descr
( "%s\n\nThis is an auto-generated description made by dh-make-drupal." %
@project.descr ).word_wrap.prefix
end
# Returns the list of files and directories at the top level of
# this project's hierarchy
def files_at_root
@filelist.map {|f| f.gsub /\/.*/, ''}.uniq
end
# Returns the changelog's filename, if one is found
def find_changelog
files_at_root.select {|f| f =~ /^(changelog|changes)/i}
end
# Returns the license's filename, if one is found
def find_license
files_at_root.select {|f| f =~ /license/i}
end
# Returns the install instructions, if one is found
def find_install
files_at_root.select {|f| f =~ /install/i}
end
# Returns the project's documentation files, if found - All the
# .txt files, excluding changelog and license
def find_docs
files_at_root.select {|f| f =~ /\.txt$/} - find_changelog - find_license - find_install
end
# The list of files to copy to the debianized package
def files_to_install
files_at_root - find_changelog - find_license - find_docs
end
# Creates the specified file inside the debian/ directory, with
# the contents received as the second parameter
def put_in_file(filename, data)
File.open(File.join(@instdir, 'debian', filename), 'w') do |f|
f.puts data
end
end
def subdirs_for(dir)
res=[dir]
ignore = ['.', '..']
Dir.open(dir).each do |subdir|
full = File.join(dir,subdir)
next if ignore.include?(subdir)
next unless FileTest.directory?(full)
res << subdirs_for(full)
end
res.flatten.uniq
end
end
# Generates the package name for a given project name and type
# (#ProjType), for the specified Drupal version.
class PackageName
def self.for(name, type, d_ver)
('drupal%s-%s-%s' % [d_ver, type.name_part, name]).gsub(/_/, '-')
end
end
# Dependency-related information is (currently?) only expressed in
# the {module}.info file inside the tarball and can thus only be
# gathered once the module is unpacked. Information in any other
# .info file (i.e. for submodules) will be expressed in Recommendations.
#
# All dependencies are expected to be on modules. If this
# contradicts reality... I'll be glad to change it, I guess :)
class Dependencies
class Irrelevant < Exception; end
# Core Drupal dependencies were gathered by:
#
# $ dpkg -L drupal5 | grep ^/usr/share/drupal./modules |cut -d / -f 6|sort|uniq|grep -v README
# (and equivalent for drupal6, drupal7).
CoreDrupalModules = {'5' => %w(aggregator block blog blogapi book color
comment contact drupal filter forum help legacy locale menu node path
ping poll profile search statistics system taxonomy throttle tracker
upload user watchdog),
'6' => %w(aggregator block blog blogapi book color comment contact dblog
filter forum help locale menu node openid path php ping poll profile
search statistics syslog system taxonomy throttle tracker translation
trigger update upload user),
'7' => %w(aggregator block blog book color comment contact contextual
dashboard dblog field field_ui file filter forum help image locale menu
node openid overlay path php poll profile rdf search shortcut
simpletest statistics syslog system taxonomy toolbar tracker
translation trigger update user)
}
# Builds the dependency lists from the information declared in the
# project's .info file
def initialize(release, basedir)
@d_ver = release.drupal_version
@depends = ['${misc:Depends}', 'drupal%s' % @d_ver]
begin
filename = File.join(basedir, '%s.info' % release.project.name)
File.open(filename).each_line { |info_fh| parse_depends(info_fh) }
rescue Errno::EACCES, Errno::ENOENT
Logger.instance.warn(('Expected .info file (%s) not found or not ' +
'readable. Cannot fetch dependency ' +
'information.') % filename)
end
end
# Fetches the list of Debian package dependencies. It is handed
# back as an array.
def get
@depends
end
private
# Parse the dependencies declared in the .info file; skip those
# which are part of the core Drupal installation
def parse_depends(line)
begin
raise Irrelevant unless line and
line =~ /dependencies\[.*\]\s*=\s*(.*)\n?/
# Some modules add what we would regard to as garbage to the
# dependencies - Unneeded quoting is the most bothering
# example. So, clean up the dependency, leaving only
# alphanumeric and hyphens. Oh, and underscores are converted
# to hyphens while we are at it.
dep = $1.gsub(/\s.*/,'').gsub(/[^a-zA-Z0-9_-]/,'').gsub(/_/, '-')
begin
if CoreDrupalModules[@d_ver].include?(dep)
Logger.instance.debug(('Declared dependency %s is part of ' +
'Drupal %s core - Skipping') %
[dep, @d_ver])
raise Irrelevant
end
rescue NoMethodError
Logger.instance.warn(('No list of core modules available for ' +
'Drupal version %s - Cannot infer what ' +
'to exclude, including everything.') % @d_ver)
end
dep_pkg = PackageName.for(dep, ProjType.new('Modules'), @d_ver)
Logger.instance.info('Adding dependency on %s' % dep_pkg)
@depends << dep_pkg
rescue Irrelevant
# Just skip it, it's irrelevant!
end
end
end
# Recommended packages will be handled much as dependencies - For
# modules which have several .info files for their submodules, that
# information will be reported only as a recommendation.
class Recommendations < Dependencies
require 'find'
def initialize(release, basedir)
@d_ver = release.drupal_version
@depends = []
Find.find(basedir) do |filename|
next unless filename =~ /\.info$/
next if filename =~ /#{release.project.name}\.info/
begin
File.open(filename).each_line { |info_fh| parse_depends(info_fh) }
rescue Errno::EACCES, Errno::ENOENT
Logger.instance.warn(('Expected .info file (%s) not found or not ' +
'readable. Cannot fetch full ' +
'recommendations information.') % filename)
end
end
# Remove duplicate dependencies; some submodules will also
# depend on the master package, remove that dependency as well
@depends = @depends.sort.uniq - [PackageName.for(release.project.name,
release.project.p_type,
@d_ver) ]
end
end
# Stores the basic settings on how to treat the different kind of
# projects available through the Drupal website.
#
# Currently we are only dealing with modules, themes and
# translations. The other available types (theme engines,
# installation profiles and drupal project) are outside our scope -
# although for some of them, this code could be trivially expanded.
class ProjType
Known = {'Modules' => {:human => 'Modules',
:name_part => 'mod',
:ck_tar_in_dir => true,
:dir => 'modules'
},
'Themes' => {:human => 'Themes',
:name_part => 'thm',
:ck_tar_in_dir => true,
:dir => 'themes'},
'Translations' => {:human => 'Translations',
:name_part => 'trans',
:ck_tar_in_dir => false,
:dir => ''}
}
# Takes the type string (as reported by each project in the
# 'Breadcrumbs' of its project page). If the key is not defined,
# raises a NameError exception.
def initialize(key)
raise NameError, "Unknown project type #{key}" unless Known.has_key?(key)
@key = key
end
# Human-readable name
def human; Known[@key][:human];end
# Directory part to store projects in (inside the Drupal root)
def dir; Known[@key][:dir]; end
# The particle to add in the generated package name
def name_part; Known[@key][:name_part];end
# Whether to check the tarball structure for this particular project type
def ck_tar_in_dir?; Known[@key][:ck_tar_in_dir]; end
end
class Author
attr_accessor :name, :info_url
def self.fetch_from(url)
auth = self.new
begin
doc = Nokogiri::HTML(open(url, 'User-Agent' => "dh-make-drupal %s" % [Version]))
rescue OpenURI::HTTPError
raise IOError, "Could not open author information site at #{url}: " + $!
end
auth.info_url = url
auth.name = doc.search('h1#page-title').inner_text
auth
end
end
class Project
class UnknownProjectType < RuntimeError;end
attr_accessor :name, :url, :p_type, :descr, :author, :creation, :html
def initialize(name)
@name = name
end
def fetch_data
@url = "https://drupal.org/project/#{@name}"
Logger.instance.debug "Fetching project information from #{@url}"
begin
@html = Nokogiri::HTML(open(@url, 'User-Agent' => "dh-make-drupal %s" % [Version]))
rescue OpenURI::HTTPError
raise IOError, "Could not open #{name} project website at #{@url}: " + $!.to_s
end
# Get the project description. Fetch only the first paragraph -
# This is usually enough for the .deb, and it should be
# hand-tuned if needed.
@descr = @html.search('div.content p')[0].inner_text
# When was the project created?
@creation = Time.parse(@html.search('div.submitted').inner_text)
# Project author: We can only get the "first" author (the one
# that uploaded the node to Drupal, AFAICT). Still, we do what
# we can.
relative_url = @html.search('div.submitted a')[0].get_attribute('href').gsub( /^\//, '')
@author = Author.fetch_from('https://drupal.org/%s' % relative_url)
# Which kind of project is this? We get the active tab in the
# 'links' menu.
#
# Note that support for translations seems to be moving to a
# different infrastructure. As it is today, we can still work
# with translations (although they don't set an "active" link -
# but they are easy to heuristically spot ;-) ). We will later
# see if a stronger change is needed - It works fine as it is
# right now.
begin
@p_type = ProjType.new( @html.search('ul.links li.active')[0].inner_text )
rescue NoMethodError
begin
trans_url = "https://localize.drupal.org/translate/languages/#{@name}"
if trans = open(trans_url)
@p_type = ProjType.new('Translations')
trans_warning = "Translations are probably outdated\n" +
"Please compare module with #{trans_url} and\n" +
"https://localize.drupal.org/translate/downloads"
@descr += "\n .\n#{trans_warning}"
Logger.instance.warn trans_warning
end
rescue OpenURI::HTTPError
raise UnknownProjectType, 'Tried hard, cannot guess.'
end
end
Logger.instance.debug 'Project type for %s: %s' % [@name,
@p_type.human]
end
end
# Fetches the list of available versions for a given project, and
# allows for filtering it to match the user's requested criteria
class VersionsList < Array
class UnknownStatus < Exception;end
attr_accessor :project
# Builds the list of available versions for the requested project
def self.for(proj_name)
list = VersionsList.new
list.project = Project.new(proj_name)
list.project.fetch_data
st_map = {'recommended' => :recommended, 'other' => :supported,
'development' => :developer}
# We do web-scraping, although it is quite fragile, because the
# RSS feed provided by drupal.org does not show the release's status
# In the Drupal pages, releases are listed inside elements
# indicating (via their CSS classes) the status of the contained
# releases, and a table with the details.
#
# We will often get more than one element in releases - "official
# releases" and "development snapshots" are given as two
# tables. We should look for the highest (i.e. stablest) release
# we can get.
list.project.html.search('.view-project-release-download-table').
each do |div|
status=nil
div.get_attribute('class').split(/\s+/).each do |c|
if c =~ /^view-display-id-(#{st_map.keys.join('|')})/
status = st_map[$1]
break
end
end
div.search('tr').each do |tr|
rel = Release.from_tr(tr, status) or next
rel.project = list.project
list << rel
end
end
# Remove wrongly detected duplicate versions on different
# support levels (leave the lowest one, that's how Drupal's site
# is structured. Ugly :-(
duplicates = []
list.each do |item|
Logger.instance.debug('Examining item %s:%s status %s' % [item.drupal_version, item.version, item.status])
list.each do |other|
Logger.instance.debug('Examining other %s:%s status %s' % [other.drupal_version, other.version, other.status])
next if item == other or
item.version != other.version or
item.drupal_version != other.drupal_version
if other.status < item.status
Logger.instance.debug('Marking duplicate version %s (status %s)' % [item.version, item.status])
duplicates << item if other.status < item.status
end
end
end
duplicates.each do |item|
Logger.instance.debug('Removing duplicate version %s (status %s)' % [item.version, item.status])
list.delete(item)
end
list
end
# Returns all the versions available for the given Drupal version
# for the specified project. The version should be the standard
# family nomenclature used in Drupal (i.e. '4.7', '5', '6'). The
# versions are converted to strings for comparison.
def for_drupal_version(ver)
self.clone.delete_if {|item| item.drupal_version != ver.to_s}
end
# Gives the highest available version for this project available
# for the specified (first parameter) Drupal version, with the
# minimum requested stability (second parameter).
#
# If no matching versions are found, a EOFError exception will be
# raised.
def choose(drupal_ver, min_status)
Logger.instance.debug(("Going over %d available releases, " +
"searching for compatibility with Drupal %s, " +
"minimum development status %s (%d)") %
[ self.size, drupal_ver, min_status,
Release::Statuses[min_status] ])
res = self.for_drupal_version(drupal_ver).with_min_status(min_status).
sort_by {|item| item.version}
if res.empty?
raise EOFError, "No suitable version found for Drupal %s (level>=%s)" %
[drupal_ver, min_status]
end
return res[-1] # Last element: Highest available suitable version
end
# Returns all the versions available for the given project which have
# a stability status at least equal the specified status. The status
# can be :developer (lowest), :supported or :recommended (highest).
def with_min_status(min_status)
statuses = Release::Statuses
unless min = statuses[min_status.to_sym]
Logger.instance.error "Unknown status specified. Valid statuses: " +
statuses.keys.join(', ')
return nil
end
self.clone.delete_if {|item| item.status < min}
end
private
# This class should not be directly initialized from the outside -
# call VersionsList.for(project) instead
def initialize
end
end
# Represents the information for any given release of a Drupal project
class Release
Statuses = {:developer => 0, :supported => 1, :recommended => 2}
attr_accessor(:project, :drupal_version, :version, :status, :url, :date)
# Returns the list of statuses, highest first
def self.statuses
Statuses.keys.sort_by {|k| 0 - Statuses[k]}
end
# Creates a DrupalProject::Release from a drupal.org table row
# (yes, heavily dependent on their Web layout). Note that you will
# still have to explicitly 'give' this Release its project and
# type once it is created.
def self.from_tr(tr, status)
rel = self.new
rel.status = Statuses[status]
# We might receive non-interesting (i.e. empty or header)
# rows. Check first of all if we have version and link strings,
# and chicken out otherwise.
columns = tr/'td'
return nil unless columns[0] and columns[1]
# We split the full version (first column) into Drupal and
# project versions
full_ver = (columns[0]/'a').text
Logger.instance.debug "Found version %s (%s)" %
[full_ver, rel.sym_status]
unless full_ver =~ /^([\d\.]+).x-(.+)$/
Logger.instance.info "cannot parse version #{full_ver} - Ignoring"
return nil
end
rel.drupal_version = $1
rel.version = mangle($2)
begin
rel.date = Time.parse(columns[2].inner_text)
Logger.instance.debug "This release was uploaded on #{rel.date.to_s}"
rescue => err
Logger.instance.warn "Could not parse date «%s» - " +
"Registering current date" % columns[2]
rel.date = Time.now
end
rel.url = (columns[1]/'a')[0].attributes['href']
rel
end
# Returns the filename to which this release should be saved to
def dest_file
'%s_%s.orig.tar.gz' % [ PackageName.for(project.name, project.p_type,
drupal_version), version]
end
# Fetches the this project's released tar.gz, saves it with the
# filename specified by #dest_file
def save_file
Logger.instance.debug "Retreiving remote file #{@url}"
Logger.instance.debug "Attempting to save in #{dest_file}"
# Ok, unlinking is not the same as overwriting, except for
# practical purposes :)
File.unlink(dest_file) if (File.exists?(dest_file) and
Options.force_overwrite)
begin
if File.exists?(dest_file)
raise Errno::EEXIST, "Destination filename for source tarball "+
"(#{dest_file}) already exists. Cannot continue."
end
File.open(dest_file, 'w') {|f|
f.write open(url, 'User-Agent' => "dh-make-drupal %s" % [Version]).read}
rescue OpenURI::HTTPError
Logger.instance.error "Requested URI #{url} could not be retreived: " + $!
end
end
# Returns the symbolic status for this revision
def sym_status
Statuses.each {|k,v| return k if v == @status}
nil
end
def self.mangle(version)
return version unless Options.mangle_version
version.gsub(/\.x[-_.]?(dev)/, '~~\1').gsub(/[-_.]?(alpha|beta|rc)/, '~\1')
end
end
# Reports the progress of the requested operations to the user,
# according to the minimum severity level specified. Handles four
# severity levels: Error, Warning, Info and Debug - Respectively, 0,
# 1, 2 and 3.
class Logger
include Singleton
Levels = %w(E W I D)
# Private method, not meant to be called directly (this is a
# singleton object)
def initialize(level=1)
@@level = level
end
# Redefines the reporting level. If the specified level is below
# or above the meaningful levels, it will be adjusted to the
# (respectively) lowest or highest.
def level=(level)
l = level.to_i
l = 0 if l < 0
l = Levels.size if l > Levels.size
@@level = l
end
# Reports as a message as an error (priority 0). This will always
# be shown to the user.
def error(msg); say(0,msg);end
# Reports the message as a warning (priority 1)
def warn(msg); say(1,msg);end
# Reports the message as informational (priority 2)
def info(msg); say(2,msg);end
# Reports the message as debugging (priority 3)
def debug(msg); say(3,msg);end
private
def say(level, msg)
puts '%s %s' % [prefix(level), msg] if @@level >= level
end
def prefix(level)
'%s:%s' % [Levels[level], ' '*level]
end
end
end
class String
def word_wrap(maxlen=70)
self.gsub(/\t/," ").gsub(/.{1,#{maxlen}}(?:\s|\Z)/) do
($& + 5.chr).gsub(/\n\005/,"\n").gsub(/\005/,"\n")
end
end
def prefix(with=' ')
self.split(/\n/).map {|l| '%s%s' % [with, l.empty? ? '.' : l]}.join("\n")
end
end
class Application
def initialize
statuses = DrupalProject::Release::statuses
projtypes = DrupalProject::ProjType::Known.keys.sort
# Set default options
options = DrupalProject::Options
options.d_ver = 7
options.min_status = statuses[0]
options.force_overwrite = false
options.debug = 1
options.report_only = false
options.debianize = true
options.skip_build = false
options.skip_recommend = false
options.switches = '-us -uc'
options.tarball = false
options.proj_version = nil
options.proj_type = projtypes[0]
options.mangle_version = true
options.provides = nil
optparse = OptionParser.new do |opts|
opts.banner = Description
opts.version = Version
opts.on('-v', '--version') do
puts "#{$0} version #{Version}\n\n#{Copyright}\nWritten by #{Author}"
exit 0
end
opts.on( '-h', '--help', 'Display this screen' ) do
puts opts
exit 0
end
opts.on('-d VERSION', '--drupal-version', 'Drupal version'
) { |ver| options.d_ver = ver }
opts.on('-r', '--report-only',
"Check only for project availability, don't download or " +
"perform any other actions locally. Implies -D."
) { options.report_only = true }
opts.on('-f', '--force', 'Proceed even if this will overwrite ' +
'currently existing files'
) { options.force_overwrite = true }
opts.on('--debug LEVEL', 'Debug level for generated messages ' +
'(0=highest, 5=lowest)'
) {|level| options.debug = level}
opts.on('-s', '--min-status STATUS', statuses,
'Minimum status to consider for packaging. ' +
'Accepted values: %s. Defaults to %s.' %
[ statuses.map {|s| "'#{s}'"}.join(', '), statuses[0]]
) {|status| options.min_status = status}
opts.on('-D', '--dont-debianize',
'Do not attempt to debianize the project, only download the ' +
'tarball'
) { options.debianize = false }
opts.on('-b', '--no-build', 'Prepare the debianized directory, but ' +
'omit the actual package build process. This option is ' +
'incompatible with either -D and -r.'
) { options.skip_build = true }
opts.on('-R', '--no-recommends', 'Omits the generation of the ' +
'Recommends: line, which is built by scanning of ' +
'submodule-provided .info files and may be misleading or ' +
'introducing too much noise'
) { options.skip_recommend = true }
opts.on('--build-switches SWITCHES', 'Switches to pass to ' +
'dpkg-buildpackage. Defaults to "-us -uc" (do not sign the ' +
'generated package). In order not to give any switches, ' +
'specify an empty quoted string (i.e. --build-switches=\'\').'
) { |switches| options.switches = switches }
opts.on('-t', '--tarball FILE',
'Use the specified tarball as the original ' +
'project tar.gz, don\'t look for any other available versions ' +
'and don\'t download from the Drupal website. This will ' +
'require you also to provide a project version number with -V ' +
'and the project type with -T'
) { |tar| options.tarball = tar }
opts.on('-T', '--proj-type TYPE', projtypes,
'Type of project we are packaging. This option is ' +
'only meaningful when working on a local tarball (-t), and ' +
'will be ignored otherwise. Accepted values: %s. Defaults ' +
'to %s.' % [ projtypes, projtypes[0] ]
) { |type| options.proj_type = type }
opts.on('-V', '--proj_version VERSION', 'Provide a project version ' +
'number. This option is only meaningful when working on a local ' +
'tarball (-t), and will be ignored otherwise'
) { |ver| options.proj_version = ver }
opts.on('-m', '--mangle-version PATTERN', 'Debian versioning logic ' +
'includes the «~» character meaning «anything below» the ' +
'preceding version number. This is most useful when dealing ' +
'with pre-release qualificators (in order, 1.x-dev, 1.0-alpha1, '+
'1.0-beta, 1.0rc3). dh-make-drupal will try to recognize such ' +
'patterns and mangle them so they sort correctly in Debian ' +
'(and so that when a stable version is released it appears as ' +
'higher - For the above mentioned version numbers, they would ' +
'result in 1~~dev, 1.0~alpha1, 1.0~beta, 1.0~rc3). You can use ' +
'this switch to tell dh-make-drupal to omit this mangling.'
) { options.mangle_version = false }
opts.on('-P', '--provides SUBMODULES', Array, 'generate the Provides: ' +
'line, which is built from specified comma-separated ' +
'submodules. They will all be converted to what would amount ' +
'to their Debian package name -- i.e. "-P foo,bar" becomes ' +
'"Provides: drupal7-mod-foo, drupal7-mod-bar" (when building a ' +
'Drupal7 module).'
) { |list| options.provides = list }
end
optparse.parse!
options.project = ARGV[0]
if options.project.nil?
STDERR.puts "USAGE:"
STDERR.puts " #{$0} [options] project"
STDERR.puts " #{$0} --help for full invocation options"
exit 1
end
end
def run
o = DrupalProject::Options
log = DrupalProject::Logger.instance
log.level = o.debug
log.debug "Parsed options:\n#{o.to_h.to_yaml}"
begin
if o.tarball
begin
down = DrupalProject::Downloader.mock(o.project, o.d_ver,
o.tarball, o.proj_version)
rescue DrupalProject::Downloader::LackingMock
log.error 'Missing information: Project name, tarball and project ' +
'version are required when working with a local tarball.'
exit 1
rescue Errno::ENOENT
log.error 'Specified tarball (%s) does not exist, cannot continue' %
o.tarball
exit 1
end
else
down = DrupalProject::Downloader.new(o.project, o.d_ver, o.min_status)
down.download
end
deb = DrupalProject::DebianPackager.new(down)
deb.build_structure
deb.build_package(o.switches)
rescue DrupalProject::SkipRequested
# All fine, nothing to see, please move along
end
end
end
app = Application.new
app.run