lib/pdk/module/build.rb in pdk-2.4.0 vs lib/pdk/module/build.rb in pdk-2.5.0

- old
+ new

@@ -1,322 +1,322 @@ -require 'pdk' - -module PDK - module Module - class Build - def self.invoke(options = {}) - new(options).build - end - - attr_reader :module_dir - attr_reader :target_dir - - def initialize(options = {}) - @module_dir = PDK::Util::Filesystem.expand_path(options[:module_dir] || Dir.pwd) - @target_dir = PDK::Util::Filesystem.expand_path(options[:'target-dir'] || File.join(module_dir, 'pkg')) - end - - # Read and parse the values from metadata.json for the module that is - # being built. - # - # @return [Hash{String => Object}] The hash of metadata values. - def metadata - require 'pdk/module/metadata' - - @metadata ||= PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json')).data - end - - # Return the path where the built package file will be written to. - def package_file - @package_file ||= File.join(target_dir, "#{release_name}.tar.gz") - end - - # Build a module package from a module directory. - # - # @return [String] The path to the built package file. - def build - create_build_dir - - stage_module_in_build_dir - build_package - - package_file - ensure - cleanup_build_dir - end - - # Verify if there is an existing package in the target directory and prompts - # the user if they want to overwrite it. - def package_already_exists? - PDK::Util::Filesystem.exist?(package_file) - end - - # Check if the module is PDK Compatible. If not, then prompt the user if - # they want to run PDK Convert. - def module_pdk_compatible? - ['pdk-version', 'template-url'].any? { |key| metadata.key?(key) } - end - - # Return the path to the temporary build directory, which will be placed - # inside the target directory and match the release name (see #release_name). - def build_dir - @build_dir ||= File.join(target_dir, release_name) - end - - # Create a temporary build directory where the files to be included in - # the package will be staged before building the tarball. - # - # If the directory already exists, remove it first. - def create_build_dir - cleanup_build_dir - - PDK::Util::Filesystem.mkdir_p(build_dir) - end - - # Remove the temporary build directory and all its contents from disk. - # - # @return nil. - def cleanup_build_dir - PDK::Util::Filesystem.rm_rf(build_dir, secure: true) - end - - # Combine the module name and version into a Forge-compatible dash - # separated string. - # - # @return [String] The module name and version, joined by a dash. - def release_name - @release_name ||= [ - metadata['name'], - metadata['version'], - ].join('-') - end - - # Iterate through all the files and directories in the module and stage - # them into the temporary build directory (unless ignored). - # - # @return nil - def stage_module_in_build_dir - require 'find' - - Find.find(module_dir) do |path| - next if path == module_dir - - ignored_path?(path) ? Find.prune : stage_path(path) - end - end - - # Stage a file or directory from the module into the build directory. - # - # @param path [String] The path to the file or directory. - # - # @return nil. - def stage_path(path) - require 'pathname' - - relative_path = Pathname.new(path).relative_path_from(Pathname.new(module_dir)) - dest_path = File.join(build_dir, relative_path) - - validate_path_encoding!(relative_path.to_path) - - if PDK::Util::Filesystem.directory?(path) - PDK::Util::Filesystem.mkdir_p(dest_path, mode: PDK::Util::Filesystem.stat(path).mode) - elsif PDK::Util::Filesystem.symlink?(path) - warn_symlink(path) - else - validate_ustar_path!(relative_path.to_path) - PDK::Util::Filesystem.cp(path, dest_path, preserve: true) - end - rescue ArgumentError => e - raise PDK::CLI::ExitWithError, _( - '%{message} Rename the file or exclude it from the package ' \ - 'by adding it to the .pdkignore file in your module.', - ) % { message: e.message } - end - - # Check if the given path matches one of the patterns listed in the - # ignore file. - # - # @param path [String] The path to be checked. - # - # @return [Boolean] true if the path matches and should be ignored. - def ignored_path?(path) - path = path.to_s + '/' if PDK::Util::Filesystem.directory?(path) - - !ignored_files.match_paths([path], module_dir).empty? - end - - # Warn the user about a symlink that would have been included in the - # built package. - # - # @param path [String] The relative or absolute path to the symlink. - # - # @return nil. - def warn_symlink(path) - require 'pathname' - - symlink_path = Pathname.new(path) - module_path = Pathname.new(module_dir) - - PDK.logger.warn _('Symlinks in modules are not supported and will not be included in the package. Please investigate symlink %{from} -> %{to}.') % { - from: symlink_path.relative_path_from(module_path), - to: symlink_path.realpath.relative_path_from(module_path), - } - end - - # Checks if the path length will fit into the POSIX.1-1998 (ustar) tar - # header format. - # - # POSIX.1-2001 (which allows paths of infinite length) was adopted by GNU - # tar in 2004 and is supported by minitar 0.7 and above. Unfortunately - # much of the Puppet ecosystem still uses minitar 0.6.1. - # - # POSIX.1-1998 tar format does not allow for paths greater than 256 bytes, - # or paths that can't be split into a prefix of 155 bytes (max) and - # a suffix of 100 bytes (max). - # - # This logic was pretty much copied from the private method - # {Archive::Tar::Minitar::Writer#split_name}. - # - # @param path [String] the relative path to be added to the tar file. - # - # @raise [ArgumentError] if the path is too long or could not be split. - # - # @return [nil] - def validate_ustar_path!(path) - if path.bytesize > 256 - raise ArgumentError, _("The path '%{path}' is longer than 256 bytes.") % { - path: path, - } - end - - if path.bytesize <= 100 - prefix = '' - else - parts = path.split(File::SEPARATOR) - newpath = parts.pop - nxt = '' - - loop do - nxt = parts.pop || '' - break if newpath.bytesize + 1 + nxt.bytesize >= 100 - newpath = File.join(nxt, newpath) - end - - prefix = File.join(*parts, nxt) - path = newpath - end - - return unless path.bytesize > 100 || prefix.bytesize > 155 - - raise ArgumentError, _( - "'%{path}' could not be split at a directory separator into two " \ - 'parts, the first having a maximum length of 155 bytes and the ' \ - 'second having a maximum length of 100 bytes.', - ) % { path: path } - end - - # Checks if the path contains any non-ASCII characters. - # - # Java will throw an error when it encounters a path containing - # characters that are not supported by the hosts locale. In order to - # maximise compatibility we limit the paths to contain only ASCII - # characters, which should be part of any locale character set. - # - # @param path [String] the relative path to be added to the tar file. - # - # @raise [ArgumentError] if the path contains non-ASCII characters. - # - # @return [nil] - def validate_path_encoding!(path) - return unless path =~ %r{[^\x00-\x7F]} - - raise ArgumentError, _( - "'%{path}' can only include ASCII characters in its path or " \ - 'filename in order to be compatible with a wide range of hosts.', - ) % { path: path } - end - - # Creates a gzip compressed tarball of the build directory. - # - # If the destination package already exists, it will be removed before - # creating the new tarball. - # - # @return nil. - def build_package - require 'zlib' - require 'minitar' - require 'find' - - PDK::Util::Filesystem.rm_f(package_file) - - Dir.chdir(target_dir) do - begin - gz = Zlib::GzipWriter.new(File.open(package_file, 'wb')) # rubocop:disable PDK/FileOpen - tar = Minitar::Output.new(gz) - Find.find(release_name) do |entry| - entry_meta = { - name: entry, - } - - orig_mode = PDK::Util::Filesystem.stat(entry).mode - min_mode = Minitar.dir?(entry) ? 0o755 : 0o644 - - entry_meta[:mode] = orig_mode | min_mode - - if entry_meta[:mode] != orig_mode - PDK.logger.debug(_('Updated permissions of packaged \'%{entry}\' to %{new_mode}') % { - entry: entry, - new_mode: (entry_meta[:mode] & 0o7777).to_s(8), - }) - end - - Minitar.pack_file(entry_meta, tar) - end - ensure - tar.close - end - end - end - - # Select the most appropriate ignore file in the module directory. - # - # In order of preference, we first try `.pdkignore`, then `.pmtignore` - # and finally `.gitignore`. - # - # @return [String] The path to the file containing the patterns of file - # paths to ignore. - def ignore_file - @ignore_file ||= [ - File.join(module_dir, '.pdkignore'), - File.join(module_dir, '.pmtignore'), - File.join(module_dir, '.gitignore'), - ].find { |file| PDK::Util::Filesystem.file?(file) && PDK::Util::Filesystem.readable?(file) } - end - - # Instantiate a new PathSpec class and populate it with the pattern(s) of - # files to be ignored. - # - # @return [PathSpec] The populated ignore path matcher. - def ignored_files - require 'pdk/module' - require 'pathspec' - - @ignored_files ||= - begin - ignored = if ignore_file.nil? - PathSpec.new - else - PathSpec.new(PDK::Util::Filesystem.read_file(ignore_file, open_args: 'rb:UTF-8')) - end - - if File.realdirpath(target_dir).start_with?(File.realdirpath(module_dir)) - ignored = ignored.add("\/#{File.basename(target_dir)}\/") - end - - PDK::Module::DEFAULT_IGNORED.each { |r| ignored.add(r) } - - ignored - end - end - end - end -end +require 'pdk' + +module PDK + module Module + class Build + def self.invoke(options = {}) + new(options).build + end + + attr_reader :module_dir + attr_reader :target_dir + + def initialize(options = {}) + @module_dir = PDK::Util::Filesystem.expand_path(options[:module_dir] || Dir.pwd) + @target_dir = PDK::Util::Filesystem.expand_path(options[:'target-dir'] || File.join(module_dir, 'pkg')) + end + + # Read and parse the values from metadata.json for the module that is + # being built. + # + # @return [Hash{String => Object}] The hash of metadata values. + def metadata + require 'pdk/module/metadata' + + @metadata ||= PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json')).data + end + + # Return the path where the built package file will be written to. + def package_file + @package_file ||= File.join(target_dir, "#{release_name}.tar.gz") + end + + # Build a module package from a module directory. + # + # @return [String] The path to the built package file. + def build + create_build_dir + + stage_module_in_build_dir + build_package + + package_file + ensure + cleanup_build_dir + end + + # Verify if there is an existing package in the target directory and prompts + # the user if they want to overwrite it. + def package_already_exists? + PDK::Util::Filesystem.exist?(package_file) + end + + # Check if the module is PDK Compatible. If not, then prompt the user if + # they want to run PDK Convert. + def module_pdk_compatible? + ['pdk-version', 'template-url'].any? { |key| metadata.key?(key) } + end + + # Return the path to the temporary build directory, which will be placed + # inside the target directory and match the release name (see #release_name). + def build_dir + @build_dir ||= File.join(target_dir, release_name) + end + + # Create a temporary build directory where the files to be included in + # the package will be staged before building the tarball. + # + # If the directory already exists, remove it first. + def create_build_dir + cleanup_build_dir + + PDK::Util::Filesystem.mkdir_p(build_dir) + end + + # Remove the temporary build directory and all its contents from disk. + # + # @return nil. + def cleanup_build_dir + PDK::Util::Filesystem.rm_rf(build_dir, secure: true) + end + + # Combine the module name and version into a Forge-compatible dash + # separated string. + # + # @return [String] The module name and version, joined by a dash. + def release_name + @release_name ||= [ + metadata['name'], + metadata['version'], + ].join('-') + end + + # Iterate through all the files and directories in the module and stage + # them into the temporary build directory (unless ignored). + # + # @return nil + def stage_module_in_build_dir + require 'find' + + Find.find(module_dir) do |path| + next if path == module_dir + + ignored_path?(path) ? Find.prune : stage_path(path) + end + end + + # Stage a file or directory from the module into the build directory. + # + # @param path [String] The path to the file or directory. + # + # @return nil. + def stage_path(path) + require 'pathname' + + relative_path = Pathname.new(path).relative_path_from(Pathname.new(module_dir)) + dest_path = File.join(build_dir, relative_path) + + validate_path_encoding!(relative_path.to_path) + + if PDK::Util::Filesystem.directory?(path) + PDK::Util::Filesystem.mkdir_p(dest_path, mode: PDK::Util::Filesystem.stat(path).mode) + elsif PDK::Util::Filesystem.symlink?(path) + warn_symlink(path) + else + validate_ustar_path!(relative_path.to_path) + PDK::Util::Filesystem.cp(path, dest_path, preserve: true) + end + rescue ArgumentError => e + raise PDK::CLI::ExitWithError, _( + '%{message} Rename the file or exclude it from the package ' \ + 'by adding it to the .pdkignore file in your module.', + ) % { message: e.message } + end + + # Check if the given path matches one of the patterns listed in the + # ignore file. + # + # @param path [String] The path to be checked. + # + # @return [Boolean] true if the path matches and should be ignored. + def ignored_path?(path) + path = path.to_s + '/' if PDK::Util::Filesystem.directory?(path) + + !ignored_files.match_paths([path], module_dir).empty? + end + + # Warn the user about a symlink that would have been included in the + # built package. + # + # @param path [String] The relative or absolute path to the symlink. + # + # @return nil. + def warn_symlink(path) + require 'pathname' + + symlink_path = Pathname.new(path) + module_path = Pathname.new(module_dir) + + PDK.logger.warn _('Symlinks in modules are not supported and will not be included in the package. Please investigate symlink %{from} -> %{to}.') % { + from: symlink_path.relative_path_from(module_path), + to: symlink_path.realpath.relative_path_from(module_path), + } + end + + # Checks if the path length will fit into the POSIX.1-1998 (ustar) tar + # header format. + # + # POSIX.1-2001 (which allows paths of infinite length) was adopted by GNU + # tar in 2004 and is supported by minitar 0.7 and above. Unfortunately + # much of the Puppet ecosystem still uses minitar 0.6.1. + # + # POSIX.1-1998 tar format does not allow for paths greater than 256 bytes, + # or paths that can't be split into a prefix of 155 bytes (max) and + # a suffix of 100 bytes (max). + # + # This logic was pretty much copied from the private method + # {Archive::Tar::Minitar::Writer#split_name}. + # + # @param path [String] the relative path to be added to the tar file. + # + # @raise [ArgumentError] if the path is too long or could not be split. + # + # @return [nil] + def validate_ustar_path!(path) + if path.bytesize > 256 + raise ArgumentError, _("The path '%{path}' is longer than 256 bytes.") % { + path: path, + } + end + + if path.bytesize <= 100 + prefix = '' + else + parts = path.split(File::SEPARATOR) + newpath = parts.pop + nxt = '' + + loop do + nxt = parts.pop || '' + break if newpath.bytesize + 1 + nxt.bytesize >= 100 + newpath = File.join(nxt, newpath) + end + + prefix = File.join(*parts, nxt) + path = newpath + end + + return unless path.bytesize > 100 || prefix.bytesize > 155 + + raise ArgumentError, _( + "'%{path}' could not be split at a directory separator into two " \ + 'parts, the first having a maximum length of 155 bytes and the ' \ + 'second having a maximum length of 100 bytes.', + ) % { path: path } + end + + # Checks if the path contains any non-ASCII characters. + # + # Java will throw an error when it encounters a path containing + # characters that are not supported by the hosts locale. In order to + # maximise compatibility we limit the paths to contain only ASCII + # characters, which should be part of any locale character set. + # + # @param path [String] the relative path to be added to the tar file. + # + # @raise [ArgumentError] if the path contains non-ASCII characters. + # + # @return [nil] + def validate_path_encoding!(path) + return unless path =~ %r{[^\x00-\x7F]} + + raise ArgumentError, _( + "'%{path}' can only include ASCII characters in its path or " \ + 'filename in order to be compatible with a wide range of hosts.', + ) % { path: path } + end + + # Creates a gzip compressed tarball of the build directory. + # + # If the destination package already exists, it will be removed before + # creating the new tarball. + # + # @return nil. + def build_package + require 'zlib' + require 'minitar' + require 'find' + + PDK::Util::Filesystem.rm_f(package_file) + + Dir.chdir(target_dir) do + begin + gz = Zlib::GzipWriter.new(File.open(package_file, 'wb')) # rubocop:disable PDK/FileOpen + tar = Minitar::Output.new(gz) + Find.find(release_name) do |entry| + entry_meta = { + name: entry, + } + + orig_mode = PDK::Util::Filesystem.stat(entry).mode + min_mode = Minitar.dir?(entry) ? 0o755 : 0o644 + + entry_meta[:mode] = orig_mode | min_mode + + if entry_meta[:mode] != orig_mode + PDK.logger.debug(_('Updated permissions of packaged \'%{entry}\' to %{new_mode}') % { + entry: entry, + new_mode: (entry_meta[:mode] & 0o7777).to_s(8), + }) + end + + Minitar.pack_file(entry_meta, tar) + end + ensure + tar.close + end + end + end + + # Select the most appropriate ignore file in the module directory. + # + # In order of preference, we first try `.pdkignore`, then `.pmtignore` + # and finally `.gitignore`. + # + # @return [String] The path to the file containing the patterns of file + # paths to ignore. + def ignore_file + @ignore_file ||= [ + File.join(module_dir, '.pdkignore'), + File.join(module_dir, '.pmtignore'), + File.join(module_dir, '.gitignore'), + ].find { |file| PDK::Util::Filesystem.file?(file) && PDK::Util::Filesystem.readable?(file) } + end + + # Instantiate a new PathSpec class and populate it with the pattern(s) of + # files to be ignored. + # + # @return [PathSpec] The populated ignore path matcher. + def ignored_files + require 'pdk/module' + require 'pathspec' + + @ignored_files ||= + begin + ignored = if ignore_file.nil? + PathSpec.new + else + PathSpec.new(PDK::Util::Filesystem.read_file(ignore_file, open_args: 'rb:UTF-8')) + end + + if File.realdirpath(target_dir).start_with?(File.realdirpath(module_dir)) + ignored = ignored.add("\/#{File.basename(target_dir)}\/") + end + + PDK::Module::DEFAULT_IGNORED.each { |r| ignored.add(r) } + + ignored + end + end + end + end +end