Class: PDK::Module::Build

Inherits:
Object
  • Object
show all
Defined in:
lib/pdk/module/build.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Build

Returns a new instance of Build.



12
13
14
15
# File 'lib/pdk/module/build.rb', line 12

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

Instance Attribute Details

#module_dirObject (readonly)

Returns the value of attribute module_dir.



10
11
12
# File 'lib/pdk/module/build.rb', line 10

def module_dir
  @module_dir
end

#target_dirObject (readonly)

Returns the value of attribute target_dir.



10
11
12
# File 'lib/pdk/module/build.rb', line 10

def target_dir
  @target_dir
end

Class Method Details

.invoke(options = {}) ⇒ Object



6
7
8
# File 'lib/pdk/module/build.rb', line 6

def self.invoke(options = {})
  new(options).build
end

Instance Method Details

#buildString

Build a module package from a module directory.

Returns:

  • (String)

    The path to the built package file.



35
36
37
38
39
40
41
42
43
44
# File 'lib/pdk/module/build.rb', line 35

def build
  create_build_dir

  stage_module_in_build_dir
  build_package

  package_file
ensure
  cleanup_build_dir
end

#build_dirObject

Return the path to the temporary build directory, which will be placed inside the target directory and match the release name (see #release_name).



60
61
62
# File 'lib/pdk/module/build.rb', line 60

def build_dir
  @build_dir ||= File.join(target_dir, release_name)
end

#build_packageObject

Creates a gzip compressed tarball of the build directory.

If the destination package already exists, it will be removed before creating the new tarball.

Returns:

  • nil.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/pdk/module/build.rb', line 233

def build_package
  require 'zlib'
  require 'minitar'
  require 'find'

  PDK::Util::Filesystem.rm_f(package_file)

  Dir.chdir(target_dir) do
    gz = Zlib::GzipWriter.new(File.open(package_file, 'wb')) # rubocop:disable PDK/FileOpen
    tar = Minitar::Output.new(gz)
    Find.find(release_name) do |entry|
       = {
        name: entry
      }

      orig_mode = PDK::Util::Filesystem.stat(entry).mode
      min_mode = Minitar.dir?(entry) ? 0o755 : 0o644

      [:mode] = orig_mode | min_mode

      PDK.logger.debug(format('Updated permissions of packaged \'%{entry}\' to %{new_mode}', entry: entry, new_mode: ([:mode] & 0o7777).to_s(8))) if [:mode] != orig_mode

      Minitar.pack_file(, tar)
    end
  ensure
    tar.close
  end
end

#cleanup_build_dirObject

Remove the temporary build directory and all its contents from disk.

Returns:

  • nil.



77
78
79
# File 'lib/pdk/module/build.rb', line 77

def cleanup_build_dir
  PDK::Util::Filesystem.rm_rf(build_dir, secure: true)
end

#create_build_dirObject

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.



68
69
70
71
72
# File 'lib/pdk/module/build.rb', line 68

def create_build_dir
  cleanup_build_dir

  PDK::Util::Filesystem.mkdir_p(build_dir)
end

#ignore_fileString

Select the most appropriate ignore file in the module directory.

In order of preference, we first try ‘.pdkignore`, then `.pmtignore` and finally `.gitignore`.

Returns:

  • (String)

    The path to the file containing the patterns of file paths to ignore.



269
270
271
272
273
274
275
# File 'lib/pdk/module/build.rb', line 269

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

#ignored_filesPathSpec

Instantiate a new PathSpec class and populate it with the pattern(s) of files to be ignored.

Returns:

  • (PathSpec)

    The populated ignore path matcher.



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/pdk/module/build.rb', line 281

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

      ignored = ignored.add("/#{File.basename(target_dir)}/") if File.realdirpath(target_dir).start_with?(File.realdirpath(module_dir))

      PDK::Module::DEFAULT_IGNORED.each { |r| ignored.add(r) }

      ignored
    end
end

#ignored_path?(path) ⇒ Boolean

Check if the given path matches one of the patterns listed in the ignore file.

Parameters:

  • path (String)

    The path to be checked.

Returns:

  • (Boolean)

    true if the path matches and should be ignored.



138
139
140
141
142
# File 'lib/pdk/module/build.rb', line 138

def ignored_path?(path)
  path = "#{path}/" if PDK::Util::Filesystem.directory?(path)

  !ignored_files.match_paths([path], module_dir).empty?
end

#metadataHash{String => Object}

Read and parse the values from metadata.json for the module that is being built.

Returns:

  • (Hash{String => Object})

    The hash of metadata values.



21
22
23
24
25
# File 'lib/pdk/module/build.rb', line 21

def 
  require 'pdk/module/metadata'

  @metadata ||= PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json')).data
end

#module_pdk_compatible?Boolean

Check if the module is PDK Compatible. If not, then prompt the user if they want to run PDK Convert.

Returns:

  • (Boolean)


54
55
56
# File 'lib/pdk/module/build.rb', line 54

def module_pdk_compatible?
  ['pdk-version', 'template-url'].any? { |key| .key?(key) }
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.

Returns:

  • (Boolean)


48
49
50
# File 'lib/pdk/module/build.rb', line 48

def package_already_exists?
  PDK::Util::Filesystem.exist?(package_file)
end

#package_fileObject

Return the path where the built package file will be written to.



28
29
30
# File 'lib/pdk/module/build.rb', line 28

def package_file
  @package_file ||= File.join(target_dir, "#{release_name}.tar.gz")
end

#release_nameString

Combine the module name and version into a Forge-compatible dash separated string.

Returns:

  • (String)

    The module name and version, joined by a dash.



85
86
87
88
89
90
# File 'lib/pdk/module/build.rb', line 85

def release_name
  @release_name ||= [
    ['name'],
    ['version']
  ].join('-')
end

#stage_module_in_build_dirObject

Iterate through all the files and directories in the module and stage them into the temporary build directory (unless ignored).

Returns:

  • nil



96
97
98
99
100
101
102
103
104
# File 'lib/pdk/module/build.rb', line 96

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_path(path) ⇒ Object

Stage a file or directory from the module into the build directory.

Parameters:

  • path (String)

    The path to the file or directory.

Returns:

  • nil.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/pdk/module/build.rb', line 111

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, format('%{message} Rename the file or exclude it from the package ' \
                                        'by adding it to the .pdkignore file in your module.', message: e.message)
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.

Parameters:

  • path (String)

    the relative path to be added to the tar file.

Returns:

  • (nil)

Raises:

  • (ArgumentError)

    if the path contains non-ASCII characters.



220
221
222
223
224
225
# File 'lib/pdk/module/build.rb', line 220

def validate_path_encoding!(path)
  return unless /[^\x00-\x7F]/.match?(path)

  raise ArgumentError, format("'%{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

#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.

Parameters:

  • path (String)

    the relative path to be added to the tar file.

Returns:

  • (nil)

Raises:

  • (ArgumentError)

    if the path is too long or could not be split.



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/pdk/module/build.rb', line 179

def validate_ustar_path!(path)
  raise ArgumentError, format("The path '%{path}' 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}' 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 the user about a symlink that would have been included in the built package.

Parameters:

  • path (String)

    The relative or absolute path to the symlink.

Returns:

  • nil.



150
151
152
153
154
155
156
157
158
# File 'lib/pdk/module/build.rb', line 150

def warn_symlink(path)
  require 'pathname'

  symlink_path = Pathname.new(path)
  module_path = Pathname.new(module_dir)

  PDK.logger.warn format('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