Class: Puppet::Modulebuilder::Builder
- Inherits:
-
Object
- Object
- Puppet::Modulebuilder::Builder
- Defined in:
- lib/puppet/modulebuilder/builder.rb
Overview
Class to build Puppet Modules from source
Constant Summary collapse
- IGNORED =
Due to the way how PathSpec generates the regular expression, ‘/*` doesn’t match directories starting with a dot, so we need ‘/.*` as well.
[ '/**', '/.*', '!/CHANGELOG*', '!/LICENSE', '!/README*', '!/REFERENCE.md', '!/bolt_plugin.json', '!/data/**', '!/docs/**', '!/examples/**', '!/facts.d/**', '!/files/**', '!/functions/**', '!/hiera.yaml', '!/lib/**', '!/locales/**', '!/manifests/**', '!/metadata.json', '!/plans/**', '!/scripts/**', '!/tasks/**', '!/templates/**', '!/types/**', ].freeze
Instance Attribute Summary collapse
-
#destination ⇒ Object
readonly
Returns the value of attribute destination.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#release_name ⇒ String
The release name is used for the build directory and resulting package file.
Instance Method Summary collapse
-
#build ⇒ String
Build a module package from a module directory.
- #build_context ⇒ Object
-
#build_dir ⇒ Object
Return the path to the temporary build directory, which will be placed inside the target directory and match the release name.
-
#build_package ⇒ Object
Creates a gzip compressed tarball of the build directory.
-
#cleanup_build_dir ⇒ Object
Remove the temporary build directory and all its contents from disk.
- #copy_mtime(path) ⇒ Object
-
#create_build_dir ⇒ Object
Create a temporary build directory where the files to be included in the package will be staged before building the tarball.
-
#ignored_files ⇒ PathSpec
Instantiate a new PathSpec class and populate it with the pattern(s) of files to be ignored.
-
#ignored_path?(path) ⇒ Boolean
Check if the given path matches one of the patterns listed in the ignore file.
-
#initialize(source, destination = nil, logger = nil) ⇒ Builder
constructor
A new instance of Builder.
-
#metadata ⇒ Hash{String => Object}
Read and parse the values from metadata.json for the module that is being built.
-
#package_already_exists? ⇒ Boolean
Verify if there is an existing package in the target directory and prompts the user if they want to overwrite it.
-
#package_file ⇒ Object
Return the path where the built package file will be written to.
-
#source ⇒ String
The source to build the module from.
-
#stage_module_in_build_dir ⇒ Object
Iterate through all the files and directories in the module and stage them into the temporary build directory (unless ignored).
-
#stage_path(path) ⇒ Object
Stage a file or directory from the module into the build directory.
-
#validate_path_encoding!(path) ⇒ nil
Checks if the path contains any non-ASCII characters.
-
#validate_ustar_path!(path) ⇒ nil
Checks if the path length will fit into the POSIX.1-1998 (ustar) tar header format.
-
#warn_symlink(path) ⇒ Object
Warn the user about a symlink that would have been included in the built package.
Constructor Details
#initialize(source, destination = nil, logger = nil) ⇒ Builder
Returns a new instance of Builder.
39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/puppet/modulebuilder/builder.rb', line 39 def initialize(source, destination = nil, logger = nil) unless logger.nil? || logger.is_a?(Logger) raise ArgumentError, format('logger is expected to be nil or a Logger. Got %<klass>s', klass: logger.class) end @source_validated = false @source = source @destination = destination.nil? ? File.join(source, 'pkg') : destination @logger = logger.nil? ? ::Logger.new(File.open(File::NULL, 'w')) : logger end |
Instance Attribute Details
#destination ⇒ Object (readonly)
Returns the value of attribute destination.
37 38 39 |
# File 'lib/puppet/modulebuilder/builder.rb', line 37 def destination @destination end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
37 38 39 |
# File 'lib/puppet/modulebuilder/builder.rb', line 37 def logger @logger end |
#release_name ⇒ String
The release name is used for the build directory and resulting package file.
The default combines the module name and version into a Forge-compatible dash separated string. Unless you have an unusual use case this isn’t set manually.
329 330 331 332 333 334 |
# File 'lib/puppet/modulebuilder/builder.rb', line 329 def release_name @release_name ||= [ ['name'], ['version'], ].join('-') end |
Instance Method Details
#build ⇒ String
Build a module package from a module directory.
64 65 66 67 68 69 70 71 72 73 |
# File 'lib/puppet/modulebuilder/builder.rb', line 64 def build create_build_dir stage_module_in_build_dir build_package package_file ensure cleanup_build_dir end |
#build_context ⇒ Object
83 84 85 86 87 88 |
# File 'lib/puppet/modulebuilder/builder.rb', line 83 def build_context { parent_dir: destination, build_dir_name: release_name, }.freeze end |
#build_dir ⇒ Object
Return the path to the temporary build directory, which will be placed inside the target directory and match the release name
79 80 81 |
# File 'lib/puppet/modulebuilder/builder.rb', line 79 def build_dir @build_dir ||= File.join(build_context[:parent_dir], build_context[:build_dir_name]) end |
#build_package ⇒ Object
Creates a gzip compressed tarball of the build directory.
If the destination package already exists, it will be removed before creating the new tarball.
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/puppet/modulebuilder/builder.rb', line 214 def build_package require 'zlib' require 'minitar' require 'find' FileUtils.rm_f(package_file) # The chdir necessary us due to Minitar entry not be able to separate the filename # within the TAR versus the source filename to pack Dir.chdir(build_context[:parent_dir]) do gz = Zlib::GzipWriter.new(File.open(package_file, 'wb')) begin tar = Minitar::Output.new(gz) Find.find(build_context[:build_dir_name]) do |entry| = { name: entry, } orig_mode = File.stat(entry).mode min_mode = Minitar.dir?(entry) ? 0o755 : 0o644 [:mode] = orig_mode | min_mode if [:mode] != orig_mode logger.debug(format('Updated permissions of packaged \'%<entry>s\' to %<new_mode>s', entry: entry, new_mode: ([:mode] & 0o7777).to_s(8))) end Minitar.pack_file(, tar) end ensure tar.close end end end |
#cleanup_build_dir ⇒ Object
Remove the temporary build directory and all its contents from disk.
276 277 278 |
# File 'lib/puppet/modulebuilder/builder.rb', line 276 def cleanup_build_dir FileUtils.rm_rf(build_dir, secure: true) end |
#copy_mtime(path) ⇒ Object
150 151 152 153 154 155 156 157 158 159 |
# File 'lib/puppet/modulebuilder/builder.rb', line 150 def copy_mtime(path) require 'pathname' relative_path = Pathname.new(path).relative_path_from(Pathname.new(source)) dest_path = File.join(build_dir, relative_path) validate_path_encoding!(relative_path.to_path) fileutils_touch(dest_path, mtime: file_stat(path).mtime) end |
#create_build_dir ⇒ Object
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.
267 268 269 270 271 |
# File 'lib/puppet/modulebuilder/builder.rb', line 267 def create_build_dir cleanup_build_dir fileutils_mkdir_p(build_dir) end |
#ignored_files ⇒ PathSpec
Instantiate a new PathSpec class and populate it with the pattern(s) of files to be ignored.
254 255 256 257 258 259 260 261 |
# File 'lib/puppet/modulebuilder/builder.rb', line 254 def ignored_files require 'pathspec' ignored = PathSpec.new(IGNORED) ignored.add("/#{File.basename(destination)}/") if File.realdirpath(destination).start_with?(File.realdirpath(source)) ignored end |
#ignored_path?(path) ⇒ Boolean
Check if the given path matches one of the patterns listed in the ignore file.
167 168 169 170 171 |
# File 'lib/puppet/modulebuilder/builder.rb', line 167 def ignored_path?(path) path = "#{path}/" if File.directory?(path) ignored_files.match_path(path, source) end |
#metadata ⇒ Hash{String => Object}
Read and parse the values from metadata.json for the module that is being built.
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/puppet/modulebuilder/builder.rb', line 284 def return @metadata unless @metadata.nil? = File.join(source, 'metadata.json') unless file_exists?() raise ArgumentError, format("'%<file>s' does not exist or is not a file.", file: ) end unless file_readable?() raise ArgumentError, format("Unable to open '%<file>s' for reading.", file: ) end require 'json' begin @metadata = JSON.parse(read_file()) rescue JSON::JSONError => e raise ArgumentError, format('Invalid JSON in metadata.json: %<msg>s', msg: e.) end @metadata.freeze end |
#package_already_exists? ⇒ Boolean
Verify if there is an existing package in the target directory and prompts the user if they want to overwrite it.
317 318 319 |
# File 'lib/puppet/modulebuilder/builder.rb', line 317 def package_already_exists? file_exists?(package_file) end |
#package_file ⇒ Object
Return the path where the built package file will be written to.
311 312 313 |
# File 'lib/puppet/modulebuilder/builder.rb', line 311 def package_file @package_file ||= File.join(destination, "#{release_name}.tar.gz") end |
#source ⇒ String
The source to build the module from
54 55 56 57 58 59 |
# File 'lib/puppet/modulebuilder/builder.rb', line 54 def source return @source if @source_validated validate_source! @source = File.realpath(@source) end |
#stage_module_in_build_dir ⇒ Object
Iterate through all the files and directories in the module and stage them into the temporary build directory (unless ignored).
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/puppet/modulebuilder/builder.rb', line 94 def stage_module_in_build_dir require 'find' directories = [source] staged = Find.find(source) do |path| next if path == source if ignored_path?(path) logger.debug("Ignoring #{path} from the build") Find.prune else logger.debug("Staging #{path} for the build") directories << path if file_directory?(path) stage_path(path) end end # Reset directory mtimes. This must happen after the files have been # copied since that modifies a directory's mtime directories.each do |directory| copy_mtime(directory) end staged end |
#stage_path(path) ⇒ Object
Stage a file or directory from the module into the build directory.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/puppet/modulebuilder/builder.rb', line 126 def stage_path(path) require 'pathname' relative_path = Pathname.new(path).relative_path_from(Pathname.new(source)) dest_path = File.join(build_dir, relative_path) validate_path_encoding!(relative_path.to_path) begin if file_directory?(path) fileutils_mkdir_p(dest_path, mode: file_stat(path).mode) elsif file_symlink?(path) warn_symlink(path) else validate_ustar_path!(relative_path.to_path) fileutils_cp(path, dest_path, preserve: true) end rescue ArgumentError => e raise format( '%<message>s Rename the file or exclude it from the package by adding it to the .pdkignore file in your module.', message: e. ) end end |
#validate_path_encoding!(path) ⇒ nil
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.
201 202 203 204 205 206 |
# File 'lib/puppet/modulebuilder/builder.rb', line 201 def validate_path_encoding!(path) return unless /[^\x00-\x7F]/.match?(path) raise ArgumentError, format("'%<path>s' can only include ASCII characters in its path or " \ 'filename in order to be compatible with a wide range of hosts.', path: path) end |
#validate_ustar_path!(path) ⇒ nil
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.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/puppet/modulebuilder/builder.rb', line 357 def validate_ustar_path!(path) raise ArgumentError, format("The path '%<path>s' is longer than 256 bytes.", path: path) if path.bytesize > 256 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, \ format("'%<path>s' 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 |
#warn_symlink(path) ⇒ Object
Warn the user about a symlink that would have been included in the built package.
179 180 181 182 183 184 185 186 187 |
# File 'lib/puppet/modulebuilder/builder.rb', line 179 def warn_symlink(path) require 'pathname' symlink_path = Pathname.new(path) module_path = Pathname.new(source) logger.warn format('Symlinks in modules are not supported and will not be included in the package. Please investigate symlink %<from>s -> %<to>s.', from: symlink_path.relative_path_from(module_path), to: symlink_path.realpath.relative_path_from(module_path)) end |