From 673474f8bd8a560387faf0af55f53d796bbedd42 Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Sat, 3 Dec 2022 22:53:56 -0500 Subject: [PATCH 1/3] copy of Jordan's prototype (1967) --- lib/fpm/package/srpm.rb | 243 ++++++++++++++++++++++++++++++++++++++++ templates/srpm.erb | 233 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 lib/fpm/package/srpm.rb create mode 100644 templates/srpm.erb diff --git a/lib/fpm/package/srpm.rb b/lib/fpm/package/srpm.rb new file mode 100644 index 0000000000..f7a59c52c9 --- /dev/null +++ b/lib/fpm/package/srpm.rb @@ -0,0 +1,243 @@ +require "fpm/namespace" +require "fpm/package" +require "fpm/errors" +require "fpm/util" + +# For handling conversion +require "fpm/package/cpan" + +class FPM::Package::SRPM < FPM::Package + def output(output_path) + source_archive = ::Dir.glob(build_path("*")).select(&File.method(:file?)).first + source_archive_dirname = `tar -ztf #{Shellwords.escape(source_archive)}` \ + .split("\n").map { |path| path.split("/").first }.uniq.first + + # Generate an rpm spec with Source0: + rpmspec = template("srpm.erb").result(binding) + + # Copied from rpm.rb --- + %w(BUILD RPMS SRPMS SOURCES SPECS).each { |d| FileUtils.mkdir_p(build_path(d)) } + args = ["rpmbuild", "-bs"] + + if %x{uname -m}.chomp != self.architecture + rpm_target = self.architecture + end + + # issue #309 + if !attributes[:rpm_os].nil? + rpm_target = "#{architecture}-unknown-#{attributes[:rpm_os]}" + end + + # issue #707 + if !rpm_target.nil? + args += ["--target", rpm_target] + end + + # set the rpm dist tag + args += ["--define", "dist .#{attributes[:rpm_dist]}"] if attributes[:rpm_dist] + + args += [ + "--define", "buildroot #{build_path}/BUILD", + "--define", "_topdir #{build_path}", + "--define", "_sourcedir #{build_path}", + "--define", "_rpmdir #{build_path}/RPMS", + "--define", "_tmppath #{attributes[:workdir]}" + ] + + args += ["--sign"] if attributes[:rpm_sign?] + + if attributes[:rpm_auto_add_directories?] + fs_dirs_list = File.join(template_dir, "rpm", "filesystem_list") + fs_dirs = File.readlines(fs_dirs_list).reject { |x| x =~ /^\s*#/}.map { |x| x.chomp } + fs_dirs.concat((attributes[:auto_add_exclude_directories] or [])) + + Find.find(staging_path) do |path| + next if path == staging_path + if File.directory? path and !File.symlink? path + add_path = path.gsub(/^#{staging_path}/,'') + self.directories << add_path if not fs_dirs.include? add_path + end + end + else + self.directories = self.directories.map { |x| self.prefixed_path(x) } + alldirs = [] + self.directories.each do |path| + Find.find(File.join(staging_path, path)) do |subpath| + if File.directory? subpath and !File.symlink? subpath + alldirs << subpath.gsub(/^#{staging_path}/, '') + end + end + end + self.directories = alldirs + end + + # include external config files + (attributes[:config_files] or []).each do |conf| + dest_conf = File.join(staging_path, conf) + + if File.exist?(dest_conf) + logger.debug("Using --config-file from staging area", :path => conf) + elsif File.exist?(conf) + logger.info("Copying --config-file from local path", :path => conf) + FileUtils.mkdir_p(File.dirname(dest_conf)) + FileUtils.cp_r conf, dest_conf + else + logger.error("Failed to find given --config-file", :path => conf) + raise "Could not find config file '#{conf}' in staging area or on host. This can happen if you specify `--config-file '#{conf}'` but this file does not exist in the source package and also does not exist in filesystem." + end + end + + # scan all conf file paths for files and add them + allconfigs = [] + self.config_files.each do |path| + cfg_path = File.join(staging_path, path) + raise "Config file path #{cfg_path} does not exist" unless File.exist?(cfg_path) + Find.find(cfg_path) do |p| + allconfigs << p.gsub("#{staging_path}/", '') if File.file? p + end + end + allconfigs.sort!.uniq! + + self.config_files = allconfigs.map { |x| File.join("/", x) } + + # add init script if present + (attributes[:rpm_init_list] or []).each do |init| + name = File.basename(init, ".init") + dest_init = File.join(staging_path, "etc/init.d/#{name}") + FileUtils.mkdir_p(File.dirname(dest_init)) + FileUtils.cp init, dest_init + File.chmod(0755, dest_init) + end + + (attributes[:rpm_rpmbuild_define] or []).each do |define| + args += ["--define", define] + end + + # copy all files from staging to BUILD dir + # [#1538] Be sure to preserve the original timestamps. + Find.find(staging_path) do |path| + src = path.gsub(/^#{staging_path}/, '') + dst = File.join(build_path, "BUILD", src) + copy_entry(path, dst, preserve=true) + end + + specfile = File.join(build_path("SPECS"), "#{name}.spec") + File.write(specfile, rpmspec) + + edit_file(specfile) if attributes[:edit?] + + args << specfile + + logger.info("Running rpmbuild", :args => args) + safesystem(*args) + + ::Dir["#{build_path}/SRPMS/**/*.rpm"].each do |rpmpath| + # This should only output one rpm, should we verify this? + FileUtils.cp(rpmpath, output_path) + end + end + + def converted_from(origin) + if origin == FPM::Package::CPAN + # Fun hack to find the instance of the origin class + # So we can find the build_path + input = nil + ObjectSpace.each_object { |x| input = x if x.is_a?(origin) } + if input.nil? + raise "Something bad happened. Couldn't find origin package in memory? This is a bug." + end + + # Pick the first file found, should be a tarball. + source_archive = ::Dir.glob(File.join(input.build_path, "*")).select(&File.method(:file?)).first + #FileUtils.copy_entry(source_archive, build_path) + File.link(source_archive, build_path(File.basename(source_archive))) + #FileUtils.copy_entry(source_archive, build_path) + end + end + + def summary + if !attributes[:rpm_summary] + return @description.split("\n").find { |line| !line.strip.empty? } || "_" + end + + return attributes[:rpm_summary] + end # def summary + + def prefix + if attributes[:prefix] and attributes[:prefix] != '/' + return attributes[:prefix].chomp('/') + else + return "/" + end + end # def prefix + + def to_s(format=nil) + if format.nil? + format = if attributes[:rpm_dist] + "NAME-VERSION-ITERATION.DIST.src.rpm" + else + "NAME-VERSION-ITERATION.src.rpm" + end + end + return super(format.gsub("DIST", to_s_dist)) + end # def to_s + + def to_s_dist + attributes[:rpm_dist] ? "#{attributes[:rpm_dist]}" : "DIST"; + end + + # This method ensures a default value for iteration if none is provided. + def iteration + if @iteration.kind_of?(String) and @iteration.include?("-") + logger.warn("Package iteration '#{@iteration}' includes dashes, converting" \ + " to underscores. rpmbuild does not allow the dashes in the package iteration (called 'Release' in rpm)") + @iteration = @iteration.gsub(/-/, "_") + end + + return @iteration ? @iteration : 1 + end # def iteration + + def version + if @version.kind_of?(String) and @version.include?("-") + logger.warn("Package version '#{@version}' includes dashes, converting" \ + " to underscores") + @version = @version.gsub(/-/, "_") + end + + return @version + end + + # The default epoch value must be nil, see #381 + def epoch + return @epoch if @epoch.is_a?(Numeric) + + if @epoch.nil? or @epoch.empty? + return nil + end + + return @epoch + end # def epoch + + # Handle any architecture naming conversions. + # For example, debian calls amd64 what redhat calls x86_64, this + # method fixes those types of things. + def architecture + case @architecture + when nil + return %x{uname -m}.chomp # default to current arch + when "amd64" # debian and redhat disagree on architecture names + return "x86_64" + when "arm64" # debian and redhat disagree on architecture names + return "aarch64" + when "native" + return %x{uname -m}.chomp # 'native' is current arch + when "all" + # Translate fpm "all" arch to what it means in RPM. + return "noarch" + else + return @architecture + end + end # def architecture + + public(:output) +end # class FPM::Target::Deb diff --git a/templates/srpm.erb b/templates/srpm.erb new file mode 100644 index 0000000000..a7e7209dab --- /dev/null +++ b/templates/srpm.erb @@ -0,0 +1,233 @@ +# Hello packaging friend! + +# Allow building noarch packages that contain binaries +%define _binaries_in_noarch_packages_terminate_build 0 + +<% (attributes[:rpm_filter_from_requires] or []).each do |reqfilter| -%> +%filter_from_requires <%= reqfilter %> +<% end -%> +<% (attributes[:rpm_filter_from_provides] or []).each do |provfilter| -%> +%filter_from_provides <%= provfilter %> +<% end -%> +<% if !(attributes[:rpm_filter_from_requires] or []).empty? or !(attributes[:rpm_filter_from_provides] or []).empty?-%> +%filter_setup +<% end -%> + +Name: <%= name %> +Version: <%= version %> +<% if epoch -%> +Epoch: <%= epoch %> +<% end -%> +Release: <%= iteration or 1 %><%= "%{?dist}" if attributes[:rpm_dist] %> +<%# use the first line of the description as the summary -%> +Summary: <%= summary %> +<% if !attributes[:rpm_autoreqprov?] -%> +AutoReqProv: no +<% else -%> +AutoReqProv: yes +<% end -%> +<% if attributes[:rpm_autoreq?] -%> +AutoReq: yes +<% end -%> +<% if attributes[:rpm_autoprov?] -%> +AutoProv: yes +<% end -%> +# Seems specifying BuildRoot is required on older rpmbuild (like on CentOS 5) +# fpm passes '--define buildroot ...' on the commandline, so just reuse that. +BuildRoot: %buildroot +<% if !prefix.nil? and !prefix.empty? %> +Prefix: <%= prefix %> +<% end -%> + +Group: <%= category %> +<%# Sometimes the 'license' field has multiple lines... Hack around it. + # While technically yes this means we are 'modifying' the license, + # since the job of FPM is to get shit done and that this is only + # modifying whitespace, it should be reasonably OK. -%> +License: <%= license.gsub("\n", " ") %> +<% if !vendor.nil? and !vendor.empty? -%> +Vendor: <%= vendor %> +<% end -%> +<% if !url.nil? and !url.empty? -%> +URL: <%= url %> +<%else -%> +URL: http://nourlgiven.example.com/ +<% end -%> +Packager: <%= maintainer %> + +Source0: <%= source_archive %> + +<% if !attributes[:no_depends?] -%> +<% dependencies.each do |req| -%> +Requires: <%= req %> +<% end -%> +<% (attributes[:rpm_tag] or []).each do |tag| -%> +<%= tag %> +<% end -%> +<% end -%> +<% provides.each do |prov| -%> +Provides: <%= prov %> +<% end -%> +<% conflicts.each do |conflict| -%> +Conflicts: <%= conflict %> +<% end -%> +<% replaces.each do |repl| -%> +<%# The closes equivalent in RPM to "replaces" is "Obsoletes" -%> +Obsoletes: <%= repl %> +<% end -%> +<%# rpm rejects descriptions with blank lines (even between content), so hack + around it by replacing blank lines with ' .' -%> +%description +<%= description.gsub(/^\s*$/, " .") %> + +%prep +%autosetup -n <%= source_archive_dirname %> + +%build +mkdir -p ./cpan +cpanm -L ./cpan . --installdeps + +perl Makefile.PL +make + +%install +%make_install + +<%# This next section puts any %pre, %post, %preun, %postun, %verifyscript, %pretrans or %posttrans scripts %> +<% + scriptmap = { + :rpm_verifyscript => "verifyscript", + :rpm_posttrans => "posttrans", + :rpm_pretrans => "pretrans" + } +-%> +<% if script?(:before_upgrade) or script?(:after_upgrade) -%> +<% if script?(:before_upgrade) or script?(:before_install) -%> +%pre <% if attributes[:rpm_macro_expansion?] -%><%= " -e " %> <% end %> +upgrade() { +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<% if script?(:before_upgrade) -%> +<%= script(:before_upgrade) %> +<% end -%> +} +_install() { +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<% if script?(:before_install) -%> +<%= script(:before_install) %> +<% end -%> +} +if [ "${1}" -eq 1 ] +then + # "before install" goes here + _install +elif [ "${1}" -gt 1 ] +then + # "before upgrade" goes here + upgrade +fi +<% end -%> +<% if script?(:after_upgrade) or script?(:after_install) -%> +%post <% if attributes[:rpm_macro_expansion?] -%><%= " -e " %> <% end %> +upgrade() { +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<% if script?(:after_upgrade) -%> +<%= script(:after_upgrade) %> +<% end -%> +} +_install() { +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<% if script?(:after_install) -%> +<%= script(:after_install) %> +<% end -%> +} +if [ "${1}" -eq 1 ] +then + # "after install" goes here + _install +elif [ "${1}" -gt 1 ] +then + # "after upgrade" goes here + upgrade +fi +<% end -%> +<% if script?(:before_remove) -%> +%preun <% if attributes[:rpm_macro_expansion?] -%><%= " -e " %> <% end %> +if [ "${1}" -eq 0 ] +then +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<%= script(:before_remove) %> +fi +<% end -%> +<% if script?(:after_remove) -%> +%postun <% if attributes[:rpm_macro_expansion?] -%><%= " -e " %> <% end %> +if [ "${1}" -eq 0 ] +then +<%# Making sure that at least one command is in the function -%> +<%# avoids a lot of potential errors, including the case that -%> +<%# the script is non-empty, but just whitespace and/or comments -%> + : +<%= script(:after_remove) %> +fi +<% end -%> +<% else + other_scriptmap = { + :before_install => "pre", + :after_install => "post", + :before_remove => "preun", + :after_remove => "postun" + } + scriptmap.merge!(other_scriptmap) + end +-%> +<% scriptmap.each do |name, rpmname| -%> +<% if script?(name) -%> +%<%= rpmname -%> <%= ' -e' if attributes[:rpm_macro_expansion?] %> +<%= script(name) %> +<% end -%> +<% end -%> + +<%# This section adds any triggers, as ordered in the command line -%> +<% + triggermap = { + :before_install => "prein", + :after_install => "in", + :before_uninstall => "un", + :after_target_uninstall => "postun" + } + triggermap.each do |name, rpmtype| + (attributes["rpm_trigger_#{name}".to_sym] or []).each do |trigger_name, trigger_script, trigger_scriptprog| -%> +%trigger<%= rpmtype -%> <%= trigger_scriptprog -%> -- <%= trigger_name %> +<%= trigger_script %> +<% end -%> +<% end -%> + +%files +%defattr(<%= attributes[:rpm_defattrfile] %>,<%= attributes[:rpm_user] || "root" %>,<%= attributes[:rpm_group] || "root" %>,<%= attributes[:rpm_defattrdir] %>) +<%# Output config files and then regular files. -%> +<% config_files.each do |path| -%> +%config(noreplace) <%= rpm_file_entry(path) %> +<% end -%> +<%# list directories %> +<% directories.each do |path| -%> +%dir <%= rpm_file_entry(path) %> +<% end -%> +<%# list only files, not directories? -%> +/ + +%changelog +<%= attributes[:rpm_changelog] %> From 769603506f4f19235f85c143b90df290120fbaa2 Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Thu, 8 Dec 2022 14:56:25 -0500 Subject: [PATCH 2/3] Upgraded the prototype --- lib/fpm/package.rb | 33 +++++++++++++++++++++++++++++---- lib/fpm/package/cpan.rb | 11 +++++++++++ lib/fpm/package/gem.rb | 6 ++++++ lib/fpm/package/srpm.rb | 35 +++++++++++++++++------------------ templates/srpm.erb | 8 ++------ 5 files changed, 65 insertions(+), 28 deletions(-) diff --git a/lib/fpm/package.rb b/lib/fpm/package.rb index 1bbbd5d7af..878fd70b18 100644 --- a/lib/fpm/package.rb +++ b/lib/fpm/package.rb @@ -98,6 +98,17 @@ def to_s # a summary or description of the package attr_accessor :description + # the source code archive + attr_accessor :source_archive + + # Shell code for building this package from source. (inserted into a + # source-package's spec file). + attr_accessor :build_procedure + + # Shell code for installing this package to host OS (inserted into a + # source-package's spec file). + attr_accessor :install_procedure + # hash of scripts for maintainer/package scripts (postinstall, etc) # # The keys are :before_install, etc @@ -201,10 +212,11 @@ def convert(klass) # copy other bits ivars = [ - :@architecture, :@category, :@config_files, :@conflicts, - :@dependencies, :@description, :@epoch, :@iteration, :@license, :@maintainer, - :@name, :@provides, :@replaces, :@scripts, :@url, :@vendor, :@version, - :@directories, :@staging_path, :@attrs + :@architecture, :@build_procedure, :@category, :@config_files, :@conflicts, + :@dependencies, :@description, :@epoch, :@install_procedure, :@iteration, + :@license, :@maintainer, :@name, :@provides, :@replaces, :@scripts, :@url, + :@vendor, :@version, :@directories, :@source_archive, :@staging_path, + :@attrs ] ivars.each do |ivar| #logger.debug("Copying ivar", :ivar => ivar, :value => instance_variable_get(ivar), @@ -550,6 +562,19 @@ def provides=(value) end end + def ensure_source_package_capable + required = [:@source_archive, :@build_procedure, :@install_procedure] + + required.each do |req| + unless instance_variable_get(req) + raise "#{self.class.name} does not yet support creating #{self.type} " \ + "source-based packages. Missing required instance variable #{req}." + end + end + + return true + end # def ensure_source_package_capable + # General public API public(:type, :initialize, :convert, :input, :output, :to_s, :cleanup, :files, :version, :script, :provides=) diff --git a/lib/fpm/package/cpan.rb b/lib/fpm/package/cpan.rb index a541efc18f..bcac942998 100644 --- a/lib/fpm/package/cpan.rb +++ b/lib/fpm/package/cpan.rb @@ -231,6 +231,11 @@ def input(package) # Empty install_base to avoid local::lib being used. "--install_base", "") end + + # used for creating source-based packages (such as SRPM) + self.build_procedure = "perl Build.PL\n./Build" + self.install_procedure = "./Build install" + elsif File.exist?("Makefile.PL") if attributes[:cpan_perl_lib_path] perl_lib_path = attributes[:cpan_perl_lib_path] @@ -251,6 +256,9 @@ def input(package) safesystem(*(make + ["test"])) if attributes[:cpan_test?] safesystem(*(make + ["DESTDIR=#{staging_path}", "install"])) + # used for creating source-based packages (such as SRPM) + self.build_procedure = "perl Makefile.PL\nmake" + self.install_procedure = "make install" else raise FPM::InvalidPackageConfiguration, @@ -362,6 +370,9 @@ def download(metadata, cpan_version=nil) #response.read_body { |c| fd.write(c) } fd.write(response.body) end + + self.source_archive = build_path(tarball) + return build_path(tarball) end # def download diff --git a/lib/fpm/package/gem.rb b/lib/fpm/package/gem.rb index 26a45ad419..acb29a699a 100644 --- a/lib/fpm/package/gem.rb +++ b/lib/fpm/package/gem.rb @@ -131,6 +131,8 @@ def download(gem_name, gem_version=nil) raise "Unexpected number of gem files in #{download_dir}, #{gem_files.length} should be 1" end + self.source_archive = gem_files.first + return gem_files.first end # def download @@ -217,6 +219,10 @@ def load_package_info(gem_path) end end # runtime_dependencies end #no_auto_depends + + # in case we are building a source-based package + self.build_procedure = "" + self.install_procedure = "gem install #{File.basename(gem_path)}" end # def load_package_info def install_to_staging(gem_path) diff --git a/lib/fpm/package/srpm.rb b/lib/fpm/package/srpm.rb index f7a59c52c9..6d20d7155e 100644 --- a/lib/fpm/package/srpm.rb +++ b/lib/fpm/package/srpm.rb @@ -8,9 +8,9 @@ class FPM::Package::SRPM < FPM::Package def output(output_path) - source_archive = ::Dir.glob(build_path("*")).select(&File.method(:file?)).first - source_archive_dirname = `tar -ztf #{Shellwords.escape(source_archive)}` \ - .split("\n").map { |path| path.split("/").first }.uniq.first + ensure_source_package_capable() + + source_archive_dirname = source_archive_dirname(source_archive) # Generate an rpm spec with Source0: rpmspec = template("srpm.erb").result(binding) @@ -138,21 +138,7 @@ def output(output_path) end def converted_from(origin) - if origin == FPM::Package::CPAN - # Fun hack to find the instance of the origin class - # So we can find the build_path - input = nil - ObjectSpace.each_object { |x| input = x if x.is_a?(origin) } - if input.nil? - raise "Something bad happened. Couldn't find origin package in memory? This is a bug." - end - - # Pick the first file found, should be a tarball. - source_archive = ::Dir.glob(File.join(input.build_path, "*")).select(&File.method(:file?)).first - #FileUtils.copy_entry(source_archive, build_path) - File.link(source_archive, build_path(File.basename(source_archive))) - #FileUtils.copy_entry(source_archive, build_path) - end + File.link(self.source_archive, build_path(File.basename(self.source_archive))) end def summary @@ -171,6 +157,19 @@ def prefix end end # def prefix + def source_archive_dirname(source_archive) + basename = File.basename(source_archive) + + case basename + when /\.tar\.gz$/ + return basename.sub(/\.tar\.gz$/, '') + when /\.gem$/ + return basename.sub(/\.gem$/, '') + else + raise "Unrecognized file extension in '#{basename}'. This is probably a bug." + end + end # def source_archive_dirname + def to_s(format=nil) if format.nil? format = if attributes[:rpm_dist] diff --git a/templates/srpm.erb b/templates/srpm.erb index a7e7209dab..d70676f6a9 100644 --- a/templates/srpm.erb +++ b/templates/srpm.erb @@ -84,14 +84,10 @@ Obsoletes: <%= repl %> %autosetup -n <%= source_archive_dirname %> %build -mkdir -p ./cpan -cpanm -L ./cpan . --installdeps - -perl Makefile.PL -make +<%= build_procedure %> %install -%make_install +<%= install_procedure %> <%# This next section puts any %pre, %post, %preun, %postun, %verifyscript, %pretrans or %posttrans scripts %> <% From 1514c640713d467e4babf162303f1c96deb69d12 Mon Sep 17 00:00:00 2001 From: Nicholas Hubbard Date: Sat, 14 Jan 2023 21:13:19 -0500 Subject: [PATCH 3/3] SDEB prototype --- lib/fpm/package.rb | 11 +- lib/fpm/package/cpan.rb | 3 + lib/fpm/package/gem.rb | 1 + lib/fpm/package/sdeb.rb | 1067 ++++++++++++++++++++++++++++++++++++ templates/sdeb/control.erb | 8 + templates/sdeb/dsc.erb | 15 + templates/sdeb/rules.erb | 4 + 7 files changed, 1105 insertions(+), 4 deletions(-) create mode 100644 lib/fpm/package/sdeb.rb create mode 100644 templates/sdeb/control.erb create mode 100644 templates/sdeb/dsc.erb create mode 100644 templates/sdeb/rules.erb diff --git a/lib/fpm/package.rb b/lib/fpm/package.rb index 878fd70b18..7715351458 100644 --- a/lib/fpm/package.rb +++ b/lib/fpm/package.rb @@ -109,6 +109,9 @@ def to_s # source-package's spec file). attr_accessor :install_procedure + # Arguments to pass to dh in a SDEB's debian/rules file. + attr_accessor :dh_args + # hash of scripts for maintainer/package scripts (postinstall, etc) # # The keys are :before_install, etc @@ -213,10 +216,10 @@ def convert(klass) # copy other bits ivars = [ :@architecture, :@build_procedure, :@category, :@config_files, :@conflicts, - :@dependencies, :@description, :@epoch, :@install_procedure, :@iteration, - :@license, :@maintainer, :@name, :@provides, :@replaces, :@scripts, :@url, - :@vendor, :@version, :@directories, :@source_archive, :@staging_path, - :@attrs + :@dependencies, :@description, :@dh_args, :@epoch, :@install_procedure, + :@iteration, :@license, :@maintainer, :@name, :@provides, :@replaces, + :@scripts, :@url, :@vendor, :@version, :@directories, :@source_archive, + :@staging_path, :@attrs ] ivars.each do |ivar| #logger.debug("Copying ivar", :ivar => ivar, :value => instance_variable_get(ivar), diff --git a/lib/fpm/package/cpan.rb b/lib/fpm/package/cpan.rb index bcac942998..9c84b63b27 100644 --- a/lib/fpm/package/cpan.rb +++ b/lib/fpm/package/cpan.rb @@ -266,6 +266,9 @@ def input(package) "Build.PL found" end + # dh recognizes perl code automatically + self.dh_args = '' + # Fix any files likely to cause conflicts that are duplicated # across packages. # https://github.com/jordansissel/fpm/issues/443 diff --git a/lib/fpm/package/gem.rb b/lib/fpm/package/gem.rb index acb29a699a..e52ad6ccde 100644 --- a/lib/fpm/package/gem.rb +++ b/lib/fpm/package/gem.rb @@ -223,6 +223,7 @@ def load_package_info(gem_path) # in case we are building a source-based package self.build_procedure = "" self.install_procedure = "gem install #{File.basename(gem_path)}" + self.dh_args = "--buildsystem=ruby --with ruby" end # def load_package_info def install_to_staging(gem_path) diff --git a/lib/fpm/package/sdeb.rb b/lib/fpm/package/sdeb.rb new file mode 100644 index 0000000000..b227af6653 --- /dev/null +++ b/lib/fpm/package/sdeb.rb @@ -0,0 +1,1067 @@ +require "erb" +require "fpm/namespace" +require "fpm/package" +require "fpm/errors" +require "fpm/util" +require "backports/latest" +require "fileutils" +require "digest" +require "zlib" + +# For handling conversion +require "fpm/package/cpan" +require "fpm/package/gem" + +# Support for debian source package +# +# This class supports only output of packages. +class FPM::Package::SDEB < FPM::Package + + # Map of what scripts are named. + SCRIPT_MAP = { + :before_install => "preinst", + :after_install => "postinst", + :before_remove => "prerm", + :after_remove => "postrm", + :after_purge => "postrm", + } unless defined?(SCRIPT_MAP) + + # https://www.debian.org/doc/debian-policy/ch-relationships.html#syntax-of-relationship-fields + # Example value with version relationship: libc6 (>= 2.2.1) + # Example value: libc6 + + # Package name docs here: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-source + # Package names (both source and binary, see Package) must consist only of lower case letters (a-z), + # digits (0-9), plus (+) and minus (-) signs, and periods (.). + # They must be at least two characters long and must start with an alphanumeric character. + + # Version string docs here: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version + # The format is: [epoch:]upstream_version[-debian_revision]. + # epoch - This is a single (generally small) unsigned integer + # upstream_version - must contain only alphanumerics 6 and the characters . + - ~ + # debian_revision - only alphanumerics and the characters + . ~ + VERSION_FIELD_PATTERN = / + (?:(?:[0-9]+):)? # The epoch, an unsigned int + (?:[A-Za-z0-9+~.-]+) # upstream version, probably should not contain dashes? + (?:-[A-Za-z0-9+~.]+)? # debian_revision + /x # Version field pattern + RELATIONSHIP_FIELD_PATTERN = /^ + (?[A-z0-9][A-z0-9_.-]+) + (?:\s*\((?[<>=]+)\s(?#{VERSION_FIELD_PATTERN})\))? + $/x # Relationship field pattern + + option "--ignore-iteration-in-dependencies", :flag, + "For '=' (equal) dependencies, allow iterations on the specified " \ + "version. Default is to be specific. This option allows the same " \ + "version of a package but any iteration is permitted" + + option "--build-depends", "DEPENDENCY", + "Add DEPENDENCY as a Build-Depends" do |dep| + @build_depends ||= [] + @build_depends << dep + end + + option "--pre-depends", "DEPENDENCY", + "Add DEPENDENCY as a Pre-Depends" do |dep| + @pre_depends ||= [] + @pre_depends << dep + end + + option "--dist", "DIST-TAG", "Set the deb distribution.", :default => "unstable" + + option "--custom-dsc", "FILEPATH", + "Custom version of the Debian dsc file." do |dsc| + File.expand_path(dsc) + end + + option "--custom-rules", "FILEPATH", + "Custom version of the Debian rules file." do |rules| + File.expand_path(rules) + end + + option "--custom-control", "FILEPATH", + "Custom version of the Debian control file." do |control| + File.expand_path(control) + end + + # Add custom debconf config file + option "--config", "SCRIPTPATH", + "Add SCRIPTPATH as debconf config file." do |config| + File.expand_path(config) + end + + # Add custom debconf templates file + option "--templates", "FILEPATH", + "Add FILEPATH as debconf templates file." do |templates| + File.expand_path(templates) + end + + option "--installed-size", "KILOBYTES", + "The installed size, in kilobytes. If omitted, this will be calculated " \ + "automatically" do |value| + value.to_i + end + + option "--priority", "PRIORITY", + "The debian package 'priority' value.", :default => "optional" + + option "--use-file-permissions", :flag, + "Use existing file permissions when defining ownership and modes" + + option "--user", "USER", "The owner of files in this package", :default => 'root' + + option "--group", "GROUP", "The group owner of files in this package", :default => 'root' + + option "--changelog", "FILEPATH", "Add FILEPATH as debian changelog" do |file| + File.expand_path(file) + end + + option "--generate-changes", :flag, + "Generate PACKAGENAME.changes file.", + :default => false + + option "--upstream-changelog", "FILEPATH", "Add FILEPATH as upstream changelog" do |file| + File.expand_path(file) + end + + option "--recommends", "PACKAGE", "Add PACKAGE to Recommends" do |pkg| + @recommends ||= [] + @recommends << pkg + next @recommends + end + + option "--suggests", "PACKAGE", "Add PACKAGE to Suggests" do |pkg| + @suggests ||= [] + @suggests << pkg + next @suggests + end + + option "--meta-file", "FILEPATH", "Add FILEPATH to DEBIAN directory" do |file| + @meta_files ||= [] + @meta_files << File.expand_path(file) + next @meta_files + end + + option "--interest", "EVENT", "Package is interested in EVENT trigger" do |event| + @interested_triggers ||= [] + @interested_triggers << event + next @interested_triggers + end + + option "--activate", "EVENT", "Package activates EVENT trigger" do |event| + @activated_triggers ||= [] + @activated_triggers << event + next @activated_triggers + end + + option "--interest-noawait", "EVENT", "Package is interested in EVENT trigger without awaiting" do |event| + @interested_noawait_triggers ||= [] + @interested_noawait_triggers << event + next @interested_noawait_triggers + end + + option "--activate-noawait", "EVENT", "Package activates EVENT trigger" do |event| + @activated_noawait_triggers ||= [] + @activated_noawait_triggers << event + next @activated_noawait_triggers + end + + option "--field", "'FIELD: VALUE'", "Add custom field to the control file" do |fv| + @custom_fields ||= {} + field, value = fv.split(/: */, 2) + @custom_fields[field] = value + next @custom_fields + end + + option "--no-default-config-files", :flag, + "Do not add all files in /etc as configuration files by default for Debian packages.", + :default => false + + option "--auto-config-files", :flag, + "Init script and default configuration files will be labeled as " \ + "configuration files for Debian packages.", + :default => true + + option "--shlibs", "SHLIBS", "Include control/shlibs content. This flag " \ + "expects a string that is used as the contents of the shlibs file. " \ + "See the following url for a description of this file and its format: " \ + "http://www.debian.org/doc/debian-policy/ch-sharedlibs.html#s-shlibs" + + option "--init", "FILEPATH", "Add FILEPATH as an init script", + :multivalued => true do |file| + next File.expand_path(file) + end + + option "--default", "FILEPATH", "Add FILEPATH as /etc/default configuration", + :multivalued => true do |file| + next File.expand_path(file) + end + + option "--upstart", "FILEPATH", "Add FILEPATH as an upstart script", + :multivalued => true do |file| + next File.expand_path(file) + end + + option "--systemd", "FILEPATH", "Add FILEPATH as a systemd script", + :multivalued => true do |file| + next File.expand_path(file) + end + + option "--systemd-enable", :flag , "Enable service on install or upgrade", :default => false + + option "--systemd-auto-start", :flag , "Start service after install or upgrade", :default => false + + option "--systemd-restart-after-upgrade", :flag , "Restart service after upgrade", :default => true + + option "--after-purge", "FILE", + "A script to be run after package removal to purge remaining (config) files " \ + "(a.k.a. postrm purge within apt-get purge)" do |val| + File.expand_path(val) # Get the full path to the script + end # --after-purge + + option "--maintainerscripts-force-errorchecks", :flag , + "Activate errexit shell option according to lintian. " \ + "https://lintian.debian.org/tags/maintainer-script-ignores-errors.html", + :default => false + + def initialize(*args) + super(*args) + attributes[:sdeb_priority] = "optional" + end # def initialize + + private + + # Return the architecture. This will default to native if not yet set. + # It will also try to use dpkg and 'uname -m' to figure out what the + # native 'architecture' value should be. + def architecture + if @architecture.nil? or @architecture == "native" + # Default architecture should be 'native' which we'll need to ask the + # system about. + if program_in_path?("dpkg") + @architecture = %x{dpkg --print-architecture 2> /dev/null}.chomp + if $?.exitstatus != 0 or @architecture.empty? + # if dpkg fails or emits nothing, revert back to uname -m + @architecture = %x{uname -m}.chomp + end + else + @architecture = %x{uname -m}.chomp + end + end + + case @architecture + when "x86_64" + # Debian calls x86_64 "amd64" + @architecture = "amd64" + when "aarch64" + # Debian calls aarch64 "arm64" + @architecture = "arm64" + when "noarch" + # Debian calls noarch "all" + @architecture = "all" + end + return @architecture + end # def architecture + + # Get the name of this package. See also FPM::Package#name + # + # This accessor actually modifies the name if it has some invalid or unwise + # characters. + def name + if @name =~ /[A-Z]/ + logger.warn("Debian tools (dpkg/apt) don't do well with packages " \ + "that use capital letters in the name. In some cases it will " \ + "automatically downcase them, in others it will not. It is confusing." \ + " Best to not use any capital letters at all. I have downcased the " \ + "package name for you just to be safe.", + :oldname => @name, :fixedname => @name.downcase) + @name = @name.downcase + end + + if @name.include?("_") + logger.info("Debian package names cannot include underscores; " \ + "automatically converting to dashes", :name => @name) + @name = @name.gsub(/[_]/, "-") + end + + if @name.include?(" ") + logger.info("Debian package names cannot include spaces; " \ + "automatically converting to dashes", :name => @name) + @name = @name.gsub(/[ ]/, "-") + end + + return @name + end # def name + + def prefix + return (attributes[:prefix] or "/") + end # def prefix + + def version + if @version.kind_of?(String) + if @version.start_with?("v") && @version.gsub(/^v/, "") =~ /^#{VERSION_FIELD_PATTERN}$/ + logger.warn("Debian 'Version' field needs to start with a digit. I was provided '#{@version}' which seems like it just has a 'v' prefix to an otherwise-valid Debian version, I'll remove the 'v' for you.") + @version = @version.gsub(/^v/, "") + end + + if @version !~ /^#{VERSION_FIELD_PATTERN}$/ + raise FPM::InvalidPackageConfiguration, "The version looks invalid for Debian packages. Debian version field must contain only alphanumerics and . (period), + (plus), - (hyphen) or ~ (tilde). I have '#{@version}' which which isn't valid." + end + end + + return @version + end + + def output(output_path) + ensure_source_package_capable() + + self.provides = self.provides.collect { |p| fix_provides(p) } + + self.provides.each do |provide| + if !valid_provides_field?(provide) + raise FPM::InvalidPackageConfiguration, "Found invalid Provides field values (#{provide.inspect}). This is not valid in a Debian package." + end + end + output_check(output_path) + # Abort if the target path already exists. + + # If we are given --deb-shlibs but no --after-install script, we + # should implicitly create a before/after scripts that run ldconfig + if attributes[:sdeb_shlibs] + if !script?(:after_install) + logger.info("You gave --deb-shlibs but no --after-install, so " \ + "I am adding an after-install script that runs " \ + "ldconfig to update the system library cache") + scripts[:after_install] = template("deb/ldconfig.sh.erb").result(binding) + end + if !script?(:after_remove) + logger.info("You gave --deb-shlibs but no --after-remove, so " \ + "I am adding an after-remove script that runs " \ + "ldconfig to update the system library cache") + scripts[:after_remove] = template("deb/ldconfig.sh.erb").result(binding) + end + end + + if attributes[:source_date_epoch].nil? and not attributes[:source_date_epoch_default].nil? + attributes[:source_date_epoch] = attributes[:source_date_epoch_default] + end + if attributes[:source_date_epoch] == "0" + logger.error("Alas, ruby's Zlib::GzipWriter does not support setting an mtime of zero. Aborting.") + raise "#{name}: source_date_epoch of 0 not supported." + end + if not attributes[:source_date_epoch].nil? and not ar_cmd_deterministic? + logger.error("Alas, could not find an ar that can handle -D option. Try installing recent gnu binutils. Aborting.") + raise "#{name}: ar is insufficient to support source_date_epoch." + end + if not attributes[:source_date_epoch].nil? and not tar_cmd_supports_sort_names_and_set_mtime? + logger.error("Alas, could not find a tar that can set mtime and sort. Try installing recent gnu tar. Aborting.") + raise "#{name}: tar is insufficient to support source_date_epoch." + end + + attributes[:sdeb_systemd] = [] + attributes.fetch(:sdeb_systemd_list, []).each do |systemd| + name = File.basename(systemd, ".service") + dest_systemd = staging_path("lib/systemd/system/#{name}.service") + mkdir_p(File.dirname(dest_systemd)) + FileUtils.cp(systemd, dest_systemd) + File.chmod(0644, dest_systemd) + + # add systemd service name to attribute + attributes[:sdeb_systemd] << name + end + + if script?(:before_upgrade) or script?(:after_upgrade) or attributes[:sdeb_systemd].any? + puts "Adding action files" + if script?(:before_install) or script?(:before_upgrade) + scripts[:before_install] = template("deb/preinst_upgrade.sh.erb").result(binding) + end + if script?(:before_remove) or not attributes[:sdeb_systemd].empty? + scripts[:before_remove] = template("deb/prerm_upgrade.sh.erb").result(binding) + end + if script?(:after_install) or script?(:after_upgrade) or attributes[:sdeb_systemd].any? + scripts[:after_install] = template("deb/postinst_upgrade.sh.erb").result(binding) + end + if script?(:after_remove) + scripts[:after_remove] = template("deb/postrm_upgrade.sh.erb").result(binding) + end + if script?(:after_purge) + scripts[:after_purge] = template("deb/postrm_upgrade.sh.erb").result(binding) + end + end + + # There are two changelogs that may appear: + # - debian-specific changelog, which should be archived as changelog.Debian.gz + # - upstream changelog, which should be archived as changelog.gz + # see https://www.debian.org/doc/debian-policy/ch-docs.html#s-changelogs + + # Write the changelog.Debian.gz file + dest_changelog = File.join(staging_path, "usr/share/doc/#{name}/changelog.Debian.gz") + mkdir_p(File.dirname(dest_changelog)) + File.new(dest_changelog, "wb", 0644).tap do |changelog| + Zlib::GzipWriter.new(changelog, Zlib::BEST_COMPRESSION).tap do |changelog_gz| + if not attributes[:source_date_epoch].nil? + changelog_gz.mtime = attributes[:source_date_epoch].to_i + end + if attributes[:sdeb_changelog] + logger.info("Writing user-specified changelog", :source => attributes[:sdeb_changelog]) + File.new(attributes[:sdeb_changelog]).tap do |fd| + chunk = nil + # Ruby 1.8.7 doesn't have IO#copy_stream + changelog_gz.write(chunk) while chunk = fd.read(16384) + end.close + else + logger.info("Creating boilerplate changelog file") + changelog_gz.write(template("deb/changelog.erb").result(binding)) + end + end.close + end # No need to close, GzipWriter#close will close it. + + # Write the changelog.gz file (upstream changelog) + dest_upstream_changelog = File.join(staging_path, "usr/share/doc/#{name}/changelog.gz") + if attributes[:sdeb_upstream_changelog] + File.new(dest_upstream_changelog, "wb", 0644).tap do |changelog| + Zlib::GzipWriter.new(changelog, Zlib::BEST_COMPRESSION).tap do |changelog_gz| + if not attributes[:source_date_epoch].nil? + changelog_gz.mtime = attributes[:source_date_epoch].to_i + end + logger.info("Writing user-specified upstream changelog", :source => attributes[:sdeb_upstream_changelog]) + File.new(attributes[:sdeb_upstream_changelog]).tap do |fd| + chunk = nil + # Ruby 1.8.7 doesn't have IO#copy_stream + changelog_gz.write(chunk) while chunk = fd.read(16384) + end.close + end.close + end # No need to close, GzipWriter#close will close it. + end + + if File.exists?(dest_changelog) and not File.exists?(dest_upstream_changelog) + # see https://www.debian.org/doc/debian-policy/ch-docs.html#s-changelogs + File.rename(dest_changelog, dest_upstream_changelog) + end + + attributes.fetch(:sdeb_init_list, []).each do |init| + name = File.basename(init, ".init") + dest_init = File.join(staging_path, "etc/init.d/#{name}") + mkdir_p(File.dirname(dest_init)) + FileUtils.cp init, dest_init + File.chmod(0755, dest_init) + end + + attributes.fetch(:sdeb_default_list, []).each do |default| + name = File.basename(default, ".default") + dest_default = File.join(staging_path, "etc/default/#{name}") + mkdir_p(File.dirname(dest_default)) + FileUtils.cp default, dest_default + File.chmod(0644, dest_default) + end + + attributes.fetch(:sdeb_upstart_list, []).each do |upstart| + name = File.basename(upstart, ".upstart") + dest_init = staging_path("etc/init.d/#{name}") + name = "#{name}.conf" if !(name =~ /\.conf$/) + dest_upstart = staging_path("etc/init/#{name}") + mkdir_p(File.dirname(dest_upstart)) + FileUtils.cp(upstart, dest_upstart) + File.chmod(0644, dest_upstart) + + # Install an init.d shim that calls upstart + mkdir_p(File.dirname(dest_init)) + FileUtils.ln_s("/lib/init/upstart-job", dest_init) + end + + attributes.fetch(:sdeb_systemd_list, []).each do |systemd| + name = File.basename(systemd, ".service") + dest_systemd = staging_path("lib/systemd/system/#{name}.service") + mkdir_p(File.dirname(dest_systemd)) + FileUtils.cp(systemd, dest_systemd) + File.chmod(0644, dest_systemd) + end + + debiantarxz = write_debian_tarball + + dsc = write_dsc + + File.expand_path(output_path).tap do |output_path| + ::Dir.chdir(build_path) do + safesystem(*ar_cmd, output_path, dsc, source_archive, debiantarxz) + end + end + + # if a PACKAGENAME.changes file is to be created + if self.attributes[:sdeb_generate_changes?] + distribution = self.attributes[:sdeb_dist] + + # gather information about the files to distribute + files = [ output_path ] + changes_files = [] + files.each do |path| + changes_files.push({ + :name => path, + :size => File.size?(path), + :md5sum => Digest::MD5.file(path).hexdigest, + :sha1sum => Digest::SHA1.file(path).hexdigest, + :sha256sum => Digest::SHA2.file(path).hexdigest, + }) + end + + # write change infos to .changes file + changes_path = File.basename(output_path, '.deb') + '.changes' + changes_data = template("deb/deb.changes.erb").result(binding) + File.write(changes_path, changes_data) + logger.log("Created changes", :path => changes_path) + end # if deb_generate_changes + end # def output + + def converted_from(origin) + File.link(self.source_archive, build_path(File.basename(self.source_archive))) + + self.dependencies = self.dependencies.collect do |dep| + fix_dependency(dep) + end.flatten + self.provides = self.provides.collect do |provides| + fix_provides(provides) + end.flatten + + if origin == FPM::Package::CPAN + # The fpm cpan code presents dependencies and provides fields as perl(ModuleName) + # so we'll need to convert them to something debian supports. + + # Replace perl(ModuleName) > 1.0 with Debian-style perl-ModuleName (> 1.0) + perldepfix = lambda do |dep| + m = dep.match(/perl\((?[A-Za-z0-9_:]+)\)\s*(?.*$)/) + if m.nil? + # 'dep' syntax didn't look like 'perl(Name) > 1.0' + dep + else + # Also replace '::' in the perl module name with '-' + modulename = m["name"].gsub("::", "-") + + # Fix any upper-casing or other naming concerns Debian has about packages + name = "#{attributes[:cpan_package_name_prefix]}-#{modulename}" + + if m["op"].empty? + name + else + # 'dep' syntax was like this (version constraint): perl(Module) > 1.0 + "#{name} (#{m["op"]})" + end + end + end + + rejects = [ "perl(vars)", "perl(warnings)", "perl(strict)", "perl(Config)" ] + self.dependencies = self.dependencies.reject do |dep| + # Reject non-module Perl dependencies like 'vars' and 'warnings' + rejects.include?(dep) + end.collect(&perldepfix).collect(&method(:fix_dependency)) + + # Also fix the Provides field 'perl(ModuleName) = version' to be 'perl-modulename (= version)' + self.provides = self.provides.collect(&perldepfix).collect(&method(:fix_provides)) + + end # if origin == FPM::Packagin::CPAN + + if origin == FPM::Package::Deb + changelog_path = staging_path("usr/share/doc/#{name}/changelog.Debian.gz") + if File.exists?(changelog_path) + logger.debug("Found a deb changelog file, using it.", :path => changelog_path) + attributes[:sdeb_changelog] = build_path("deb_changelog") + File.open(attributes[:sdeb_changelog], "w") do |deb_changelog| + Zlib::GzipReader.open(changelog_path) do |gz| + IO::copy_stream(gz, deb_changelog) + end + end + File.unlink(changelog_path) + end + end + + if origin == FPM::Package::Deb + changelog_path = staging_path("usr/share/doc/#{name}/changelog.gz") + if File.exists?(changelog_path) + logger.debug("Found an upstream changelog file, using it.", :path => changelog_path) + attributes[:sdeb_upstream_changelog] = build_path("deb_upstream_changelog") + File.open(attributes[:sdeb_upstream_changelog], "w") do |deb_upstream_changelog| + Zlib::GzipReader.open(changelog_path) do |gz| + IO::copy_stream(gz, deb_upstream_changelog) + end + end + File.unlink(changelog_path) + end + end + + if origin == FPM::Package::Gem + # fpm's gem input will have provides as "rubygem-name = version" + # and we need to convert this to Debian-style "rubygem-name (= version)" + self.provides = self.provides.collect do |provides| + m = /^(#{attributes[:gem_package_name_prefix]})-([^\s]+)\s*=\s*(.*)$/.match(provides) + if m + "#{m[1]}-#{m[2]} (= #{m[3]})" + else + provides + end + end + end + end # def converted_from + + def debianize_op(op) + # Operators in debian packaging are <<, <=, =, >= and >> + # So any operator like < or > must be replaced + {:< => "<<", :> => ">>"}[op.to_sym] or op + end + + def fix_dependency(dep) + # Deb dependencies are: NAME (OP VERSION), like "zsh (> 3.0)" + # Convert anything that looks like 'NAME OP VERSION' to this format. + if dep =~ /[\(,\|]/ + # Don't "fix" ones that could appear well formed already. + else + # Convert ones that appear to be 'name op version' + name, op, version = dep.split(/ +/) + if !version.nil? + # Convert strings 'foo >= bar' to 'foo (>= bar)' + dep = "#{name} (#{debianize_op(op)} #{version})" + end + end + + name_re = /^[^ \(]+/ + name = dep[name_re] + if name =~ /[A-Z]/ + logger.warn("Downcasing dependency '#{name}' because deb packages " \ + " don't work so good with uppercase names") + dep = dep.gsub(name_re) { |n| n.downcase } + end + + if dep.include?("_") + logger.warn("Replacing dependency underscores with dashes in '#{dep}' because " \ + "debs don't like underscores") + dep = dep.gsub("_", "-") + end + + # Convert gem ~> X.Y.Z to '>= X.Y.Z' and << X.Y+1.0 + if dep =~ /\(~>/ + name, version = dep.gsub(/[()~>]/, "").split(/ +/)[0..1] + nextversion = version.split(".").collect { |v| v.to_i } + l = nextversion.length + if l > 1 + nextversion[l-2] += 1 + nextversion[l-1] = 0 + else + # Single component versions ~> 1 + nextversion[l-1] += 1 + end + nextversion = nextversion.join(".") + return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"] + elsif (m = dep.match(/(\S+)\s+\(!= (.+)\)/)) + # Move '!=' dependency specifications into 'Breaks' + self.attributes[:sdeb_breaks] ||= [] + self.attributes[:sdeb_breaks] << dep.gsub(/!=/,"=") + return [] + elsif (m = dep.match(/(\S+)\s+\(= (.+)\)/)) and + self.attributes[:sdeb_ignore_iteration_in_dependencies?] + # Convert 'foo (= x)' to 'foo (>= x)' and 'foo (<< x+1)' + # but only when flag --ignore-iteration-in-dependencies is passed. + name, version = m[1..2] + nextversion = version.split('.').collect { |v| v.to_i } + nextversion[-1] += 1 + nextversion = nextversion.join(".") + return ["#{name} (>= #{version})", "#{name} (<< #{nextversion})"] + elsif (m = dep.match(/(\S+)\s+\(> (.+)\)/)) + # Convert 'foo (> x) to 'foo (>> x)' + name, version = m[1..2] + return ["#{name} (>> #{version})"] + else + # otherwise the dep is probably fine + return dep.rstrip + end + end # def fix_dependency + + def valid_provides_field?(text) + m = RELATIONSHIP_FIELD_PATTERN.match(text) + if m.nil? + logger.error("Invalid relationship field for debian package: #{text}") + return false + end + + # Per Debian Policy manual, https://www.debian.org/doc/debian-policy/ch-relationships.html#syntax-of-relationship-fields + # >> The relations allowed are <<, <=, =, >= and >> for strictly earlier, earlier or equal, + # >> exactly equal, later or equal and strictly later, respectively. The exception is the + # >> Provides field, for which only = is allowed + if m["relation"] == "=" || m["relation"] == nil + return true + end + return false + end + + def valid_relationship_field?(text) + m = RELATIONSHIP_FIELD_PATTERN.match(text) + if m.nil? + logger.error("Invalid relationship field for debian package: #{text}") + return false + end + return true + end + + def fix_provides(provides) + name_re = /^[^ \(]+/ + name = provides[name_re] + if name =~ /[A-Z]/ + logger.warn("Downcasing provides '#{name}' because deb packages " \ + " don't work so good with uppercase names") + provides = provides.gsub(name_re) { |n| n.downcase } + end + + if provides.include?("_") + logger.warn("Replacing 'provides' underscores with dashes in '#{provides}' because " \ + "debs don't like underscores") + provides = provides.gsub("_", "-") + end + + if m = provides.match(/^([A-Za-z0-9_-]+)\s*=\s*(\d+.*$)/) + logger.warn("Replacing 'provides' entry #{provides} with syntax 'name (= version)'") + provides = "#{m[1]} (= #{m[2]})" + end + return provides.rstrip + end + + def write_dsc + staging_path("#{name}.dsc").tap do |dsc| + if attributes[:sdeb_custom_dsc] + logger.debug("Using '#{attributes[:sdeb_custom_dsc]}' template for the .dsc file") + dsc_data = File.read(attributes[:sdeb_custom_dsc]) + else + logger.debug("Using 'dsc.erb' template for the dsc file") + dsc_data = template("sdeb/dsc.erb").result(binding) + end + + logger.debug("Writing dsc file", :path => dsc) + File.write(dsc, dsc_data) + File.chmod(0644, dsc) + end + end # def write_dsc + + def dsc_files(algorithm="md5") + case algorithm + when "md5" + algorithm = Digest::MD5 + when "sha1" + algorithm = Digest::SHA1 + when "sha256" + algorithm = Digest::SHA256 + else + raise "Invalid digest algorithm '#{algorithm}'. Expected one of 'md5', 'sha1', 'sha256'" + end + + dsc_files = [] + ::Dir.glob("#{build_path}/*") do |f| + checksum = algorithm.send('file', f).hexdigest + size = File.size(f) + dsc_files << "#{checksum} #{size} #{File.basename(f)}" + end + return dsc_files + end # def dsc_files + + def write_debian_tarball + write_rules + write_control + write_shlibs + write_scripts + write_conffiles + write_debconf + write_meta_files + write_triggers + write_md5sums + + debiantarxz = build_path("debian.tar.xz") + + # Make the debian.tar.xz + logger.info("Creating", :path => debiantarxz, :from => debian_dir) + + args = [ tar_cmd, "-C", File.dirname(debian_dir), "-Jcf", debiantarxz, + "--owner=0", "--group=0", "--numeric-owner", File.basename(debian_dir) ] + if tar_cmd_supports_sort_names_and_set_mtime? and not attributes[:source_date_epoch].nil? + # Force deterministic file order and timestamp + args += ["--sort=name", ("--mtime=@%s" % attributes[:source_date_epoch])] + # gnu tar obeys GZIP environment variable with options for gzip; -n = forget original filename and date + args.unshift({"GZIP" => "-9n"}) + end + safesystem(*args) + return debiantarxz + ensure + logger.debug("Removing no longer needed debian dir", :path => debian_dir) + FileUtils.rm_r(debian_dir) + end # def write_debian_tarball + + def write_rules + debian_dir("rules").tap do |rules| + if attributes[:sdeb_custom_rules] + logger.debug("Using '#{attributes[:sdeb_custom_rules]}' template for the rules file") + rules_data = File.read(attributes[:sdeb_custom_rules]) + else + logger.debug("Using 'rules.erb' template for the control file") + rules_data = template("sdeb/rules.erb").result(binding) + end + + logger.debug("Writing rules file", :path => rules) + File.write(rules, rules_data) + File.chmod(0755, rules) + edit_file(rules) if attributes[:edit?] + end + end # def write_rules + + def write_control + # warn user if epoch is set + logger.warn("epoch in Version is set", :epoch => self.epoch) if self.epoch + + # calculate installed-size if necessary: + if attributes[:sdeb_installed_size].nil? + logger.info("No deb_installed_size set, calculating now.") + total = 0 + Find.find(staging_path) do |path| + stat = File.lstat(path) + next if stat.directory? + total += stat.size + end + # Per http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Installed-Size + # "The disk space is given as the integer value of the estimated + # installed size in bytes, divided by 1024 and rounded up." + attributes[:sdeb_installed_size] = total / 1024 + end + + # Write the control file + debian_dir("control").tap do |control| + if attributes[:sdeb_custom_control] + logger.debug("Using '#{attributes[:sdeb_custom_control]}' template for the control file") + control_data = File.read(attributes[:sdeb_custom_control]) + else + logger.debug("Using 'deb.erb' template for the control file") + control_data = template("sdeb/control.erb").result(binding) + end + + logger.debug("Writing control file", :path => control) + File.write(control, control_data) + File.chmod(0644, control) + edit_file(control) if attributes[:edit?] + end + end # def write_contol + + # Write out the maintainer scripts + # + # SCRIPT_MAP is a map from the package ':after_install' to debian + # 'post_install' names + def write_scripts + SCRIPT_MAP.each do |scriptname, filename| + next unless script?(scriptname) + + debian_dir(filename).tap do |controlscript| + logger.debug("Writing control script", :source => filename, :target => controlscript) + File.write(controlscript, script(scriptname)) + # deb maintainer scripts are required to be executable + File.chmod(0755, controlscript) + end + end + end # def write_scripts + + def write_conffiles + # expand recursively a given path to be put in allconfigs + def add_path(path, allconfigs) + # Strip leading / + path = path[1..-1] if path[0,1] == "/" + cfg_path = File.expand_path(path, staging_path) + Find.find(cfg_path) do |p| + if File.file?(p) + allconfigs << p.gsub("#{staging_path}/", '') + end + end + end + + # check for any init scripts or default files + inits = attributes.fetch(:sdeb_init_list, []) + defaults = attributes.fetch(:sdeb_default_list, []) + upstarts = attributes.fetch(:sdeb_upstart_list, []) + etcfiles = [] + # Add everything in /etc + begin + if !attributes[:sdeb_no_default_config_files?] && File.exists?(staging_path("/etc")) + logger.warn("Debian packaging tools generally labels all files in /etc as config files, " \ + "as mandated by policy, so fpm defaults to this behavior for deb packages. " \ + "You can disable this default behavior with --deb-no-default-config-files flag") + add_path("/etc", etcfiles) + end + rescue Errno::ENOENT + end + + return unless (config_files.any? or inits.any? or defaults.any? or upstarts.any? or etcfiles.any?) + + allconfigs = etcfiles + + # scan all conf file paths for files and add them + config_files.each do |path| + logger.debug("Checking if #{path} exists") + cfe = File.exist?("#{path}") + logger.debug("Check result #{cfe}") + begin + add_path(path, allconfigs) + rescue Errno::ENOENT + if !cfe + raise FPM::InvalidPackageConfiguration, + "Error trying to use '#{path}' as a config file in the package. Does it exist?" + else + dcl = File.join(staging_path, path) + if !File.exist?("#{dcl}") + logger.debug("Adding config file #{path} to Staging area #{staging_path}") + FileUtils.mkdir_p(File.dirname(dcl)) + FileUtils.cp_r path, dcl + else + logger.debug("Config file aready exists in staging area.") + end + end + end + end + + if attributes[:sdeb_auto_config_files?] + inits.each do |init| + name = File.basename(init, ".init") + initscript = "/etc/init.d/#{name}" + logger.debug("Add conf file declaration for init script", :script => initscript) + allconfigs << initscript[1..-1] + end + defaults.each do |default| + name = File.basename(default, ".default") + confdefaults = "/etc/default/#{name}" + logger.debug("Add conf file declaration for defaults", :default => confdefaults) + allconfigs << confdefaults[1..-1] + end + upstarts.each do |upstart| + name = File.basename(upstart, ".upstart") + upstartscript = "/etc/init/#{name}.conf" + logger.debug("Add conf file declaration for upstart script", :script => upstartscript) + allconfigs << upstartscript[1..-1] + end + end + + allconfigs.sort!.uniq! + return unless allconfigs.any? + + debian_dir("conffiles").tap do |conffiles| + File.open(conffiles, "w") do |out| + allconfigs.each do |cf| + # We need to put the leading / back. Stops lintian relative-conffile error. + out.puts("/" + cf) + end + end + File.chmod(0644, conffiles) + end + end # def write_conffiles + + def write_shlibs + return unless attributes[:sdeb_shlibs] + logger.info("Adding shlibs", :content => attributes[:sdeb_shlibs]) + File.open(debian_dir("shlibs"), "w") do |out| + out.write(attributes[:sdeb_shlibs]) + end + File.chmod(0644, debian_dir("shlibs")) + end # def write_shlibs + + def write_debconf + if attributes[:sdeb_config] + FileUtils.cp(attributes[:sdeb_config], debian_dir("config")) + File.chmod(0755, debian_dir("config")) + end + + if attributes[:sdeb_templates] + FileUtils.cp(attributes[:sdeb_templates], debian_dir("templates")) + File.chmod(0644, debian_dir("templates")) + end + end # def write_debconf + + def write_meta_files + files = attributes[:sdeb_meta_file] + return unless files + files.each do |fn| + dest = debian_dir(File.basename(fn)) + FileUtils.cp(fn, dest) + File.chmod(0644, dest) + end + end # def write_meta_files + + def write_triggers + lines = [['interest', :sdeb_interest], + ['activate', :sdeb_activate], + ['interest-noawait', :sdeb_interest_noawait], + ['activate-noawait', :sdeb_activate_noawait], + ].map { |label, attr| + (attributes[attr] || []).map { |e| "#{label} #{e}\n" } + }.flatten.join('') + + if lines.size > 0 + File.open(debian_dir("triggers"), 'a') do |f| + f.chmod 0644 + f.write "\n" if f.size > 0 + f.write lines + end + end + end # def write_triggers + + def write_md5sums + md5_sums = {} + + Find.find(staging_path) do |path| + if File.file?(path) && !File.symlink?(path) + md5 = Digest::MD5.file(path).hexdigest + md5_path = path.gsub("#{staging_path}/", "") + md5_sums[md5_path] = md5 + end + end + + if not md5_sums.empty? + File.open(debian_dir("md5sums"), "w") do |out| + md5_sums.each do |path, md5| + out.puts "#{md5} #{path}" + end + end + File.chmod(0644, debian_dir("md5sums")) + end + end # def write_md5sums + + def mkdir_p(dir) + FileUtils.mkdir_p(dir, :mode => 0755) + end # def mkdir_p + + def to_s(format=nil) + # Default format if nil + # git_1.7.9.3-1_amd64.deb + return super(format.nil? ? "NAME_FULLVERSION_ARCH.EXTENSION" : format) + end # def to_s + + def data_tar_flags + data_tar_flags = [] + if attributes[:sdeb_use_file_permissions?].nil? + if !attributes[:sdeb_user].nil? + if attributes[:sdeb_user] == 'root' + data_tar_flags += [ "--numeric-owner", "--owner", "0" ] + else + data_tar_flags += [ "--owner", attributes[:sdeb_user] ] + end + end + + if !attributes[:sdeb_group].nil? + if attributes[:sdeb_group] == 'root' + data_tar_flags += [ "--numeric-owner", "--group", "0" ] + else + data_tar_flags += [ "--group", attributes[:sdeb_group] ] + end + end + end + return data_tar_flags + end # def data_tar_flags + + def debian_dir(path=nil) + @debian_dir ||= staging_path("debian") + mkdir_p(@debian_dir) unless File.directory?(@debian_dir) + if path.nil? + return @debian_dir + else + return File.join(@debian_dir, path) + end + end # def debian_dir + + public(:input, :output, :architecture, :name, :prefix, :version, :converted_from, :to_s, :data_tar_flags) +end # class FPM::Target::SDEB diff --git a/templates/sdeb/control.erb b/templates/sdeb/control.erb new file mode 100644 index 0000000000..12c0b62086 --- /dev/null +++ b/templates/sdeb/control.erb @@ -0,0 +1,8 @@ +Source: <%= name %> +Maintainer: <%= maintainer %> +Section: <%= category %> +Priority: <%= attributes[:sdeb_priority] %> +<% if url -%> +Homepage: <%= url %> +<% end -%> +Standards-Version: 4.6.2.0 diff --git a/templates/sdeb/dsc.erb b/templates/sdeb/dsc.erb new file mode 100644 index 0000000000..fb84bdd981 --- /dev/null +++ b/templates/sdeb/dsc.erb @@ -0,0 +1,15 @@ +Format: 1.8 +Source: <%= name %> +Version: <%= "#{epoch}:" if epoch %><%= version %><%= "-" + iteration.to_s if iteration %> +Architecture: <%= architecture %> +Maintainer: <%= maintainer %> +Standards-Version: 4.6.2.0 +<% if url -%> +Homepage: <%= url %> +<% end -%> +Checksums-Sha1: +<%= dsc_files("sha1").map { |f| " #{f}" }.join("\n") %> +Checksums-Sha256: +<%= dsc_files("sha256").map { |f| " #{f}" }.join("\n") %> +Files: +<%= dsc_files("md5").map { |f| " #{f}" }.join("\n") %> diff --git a/templates/sdeb/rules.erb b/templates/sdeb/rules.erb new file mode 100644 index 0000000000..6df557448c --- /dev/null +++ b/templates/sdeb/rules.erb @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@<%= " #{dh_args}" unless dh_args.nil? || dh_args.empty? %>