class FPM::Package::APK
Support for Alpine packages (.apk files)
This class supports both input and output of packages.
Constants
- TAR_CHECKSUM_OFFSET_END
- TAR_CHECKSUM_OFFSET_START
- TAR_CHUNK_SIZE
- TAR_GID_END
- TAR_GID_START
- TAR_GNAME_END
- TAR_GNAME_START
- TAR_LENGTH_OFFSET_END
- TAR_LENGTH_OFFSET_START
- TAR_MAGIC_END
- TAR_MAGIC_START
- TAR_MAJOR_END
- TAR_MAJOR_START
- TAR_MINOR_END
- TAR_MINOR_START
- TAR_NAME_OFFSET_END
- TAR_NAME_OFFSET_START
- TAR_TYPEFLAG_OFFSET
- TAR_UID_END
- TAR_UID_START
- TAR_UNAME_END
- TAR_UNAME_START
Public Instance Methods
Source
# File lib/fpm/package/apk.rb, line 68 def architecture # "native" in apk should be "noarch" if @architecture.nil? or @architecture == "native" @architecture = "noarch" end return @architecture end
Source
# File lib/fpm/package/apk.rb, line 77 def input(input_path) logger.error("apk extraction is not yet implemented") end
Source
# File lib/fpm/package/apk.rb, line 45 def name if @name =~ /[A-Z]/ logger.warn("apk packages should not have uppercase characters in their names") @name = @name.downcase end if @name.include?("_") logger.warn("apk packages should not include underscores") @name = @name.gsub(/[_]/, "-") end if @name.include?(" ") logger.warn("apk packages should not contain spaces") @name = @name.gsub(/[ ]/, "-") end return @name end
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.
Source
# File lib/fpm/package/apk.rb, line 81 def output(output_path) output_check(output_path) control_path = build_path("control") controltar_path = build_path("control.tar") datatar_path = build_path("data.tar") FileUtils.mkdir(control_path) # data tar. tar_path(staging_path(""), datatar_path) # control tar. begin write_pkginfo(control_path) write_control_scripts(control_path) tar_path(control_path, controltar_path) ensure FileUtils.rm_r(control_path) end # concatenate the two into a real apk. begin # cut end-of-tar record from control tar cut_tar_record(controltar_path) # calculate/rewrite sha1 hashes for data tar hash_datatar(datatar_path) # concatenate the two into the final apk concat_zip_tars(controltar_path, datatar_path, output_path) end logger.warn("apk output does not currently sign packages.") logger.warn("It's recommended that your package be installed with '--allow-untrusted'") end
Source
# File lib/fpm/package/apk.rb, line 64 def prefix return (attributes[:prefix] or "/") end
Source
# File lib/fpm/package/apk.rb, line 504 def to_s(format=nil) return super("NAME_FULLVERSION_ARCH.TYPE") if format.nil? return super(format) end
FPM::Package#to_s
Private Instance Methods
Source
# File lib/fpm/package/apk.rb, line 436 def add_paxstring(ret) pax_slash = ret.rindex('/') if(pax_slash == nil) pax_slash = 0 else pax_slash = ret.rindex('/', pax_slash-1) if(pax_slash == nil || pax_slash < 0) pax_slash = 0 end end ret = ret.insert(pax_slash, "/PaxHeaders.14670/") ret = ret.sub("//", "/") return ret end
APK
adds a “PAX” magic string into most directory names. This takes an unchanged directory name and “paxifies” it.
Source
# File lib/fpm/package/apk.rb, line 348 def checksum_header(header) # blank out header checksum replace_string_range(header, TAR_CHECKSUM_OFFSET_START, TAR_CHECKSUM_OFFSET_END, ' ') # calculate new checksum checksum = 0 for i in 0..(TAR_CHUNK_SIZE-1) checksum += header.getbyte(i) end checksum = checksum.to_s(8).rjust(6, '0') header[TAR_CHECKSUM_OFFSET_START..TAR_CHECKSUM_OFFSET_END-2] = checksum header[TAR_CHECKSUM_OFFSET_END-1] = "\0" return header end
Checksums the entire contents of the given [header] Writes the resultant checksum into indices 148-155 of the same [header], and returns the modified header. 148-155 is the “size” range in a tar/ustar header.
Source
# File lib/fpm/package/apk.rb, line 296 def concat_zip_tars(apath, bpath, target_path) temp_apath = apath + "~" temp_bpath = bpath + "~" # zip each path separately Zlib::GzipWriter.open(temp_apath) do |target_writer| open(apath, "rb") do |file| until(file.eof?()) target_writer.write(file.read(4096)) end end end Zlib::GzipWriter.open(temp_bpath) do |target_writer| open(bpath, "rb") do |file| until(file.eof?()) target_writer.write(file.read(4096)) end end end # concat both into one. File.open(target_path, "wb") do |target_writer| open(temp_apath, "rb") do |file| until(file.eof?()) target_writer.write(file.read(4096)) end end open(temp_bpath, "rb") do |file| until(file.eof?()) target_writer.write(file.read(4096)) end end end end
Concatenates each of the given [apath] and [bpath] into the given [target_path]
Source
# File lib/fpm/package/apk.rb, line 172 def cut_tar_record(target_path) temporary_target_path = target_path + "~" record_length = 0 empty_records = 0 open(temporary_target_path, "wb") do |target_file| # Scan to find the location of the two contiguous null records open(target_path, "rb") do |file| until(empty_records == 2) header = file.read(TAR_CHUNK_SIZE) # clear off ownership info header = replace_ownership_headers(header, true) typeflag = header[TAR_TYPEFLAG_OFFSET] ascii_length = header[TAR_LENGTH_OFFSET_START..TAR_LENGTH_OFFSET_END] if(file.eof?()) raise StandardError.new("Invalid tar stream, eof before end-of-tar record") end if(typeflag == "\0") empty_records += 1 next end record_length = ascii_length.to_i(8) record_length = determine_record_length(record_length) target_file.write(header) target_file.write(file.read(record_length)) end end end FileUtils::mv(temporary_target_path, target_path) end
Removes the end-of-tar records from the given [target_path]. End of tar records are two contiguous empty tar records at the end of the file Taken together, they comprise 1k of null data.
Source
# File lib/fpm/package/apk.rb, line 334 def determine_record_length(record_length) sans_size = TAR_CHUNK_SIZE-1 if(record_length % TAR_CHUNK_SIZE != 0) record_length = (record_length + sans_size) & ~sans_size; end return record_length end
Rounds the given [record_length] to the nearest highest evenly-divisble number of 512.
Source
# File lib/fpm/package/apk.rb, line 218 def hash_datatar(target_path) header = extension_header = "" data = extension_data = "" record_length = extension_length = 0 empty_records = 0 temporary_file_name = target_path + "~" target_file = open(temporary_file_name, "wb") file = open(target_path, "rb") begin until(file.eof?() || empty_records == 2) header = file.read(TAR_CHUNK_SIZE) typeflag = header[TAR_TYPEFLAG_OFFSET] record_length = header[TAR_LENGTH_OFFSET_START..TAR_LENGTH_OFFSET_END].to_i(8) data = "" record_length = determine_record_length(record_length) until(data.length == record_length) data << file.read(TAR_CHUNK_SIZE) end # Clear ownership fields header = replace_ownership_headers(header, false) # If it's not a null record, do extension hash. if(typeflag != "\0") extension_header = header.dup() extension_header = replace_ownership_headers(extension_header, true) # directories have a magic string inserted into their name full_record_path = extension_header[TAR_NAME_OFFSET_START..TAR_NAME_OFFSET_END].delete("\0") full_record_path = add_paxstring(full_record_path) # hash data contents with sha1, if there is any content. if(typeflag == '5') extension_data = "" # ensure it doesn't end with a slash if(full_record_path[full_record_path.length-1] == '/') full_record_path = full_record_path.chop() end else extension_data = hash_record(data) end full_record_path = pad_string_to(full_record_path, 100) extension_header[TAR_NAME_OFFSET_START..TAR_NAME_OFFSET_END] = full_record_path extension_header[TAR_TYPEFLAG_OFFSET] = 'x' extension_header[TAR_LENGTH_OFFSET_START..TAR_LENGTH_OFFSET_END] = extension_data.length.to_s(8).rjust(12, '0') extension_header = checksum_header(extension_header) # write extension record target_file.write(extension_header) target_file.write(extension_data) else empty_records += 1 end # write header and data to target file. target_file.write(header) target_file.write(data) end FileUtils.mv(temporary_file_name, target_path) ensure file.close() target_file.close() end end
Rewrites the tar file located at the given [target_tar_path] to have its record headers use a simple checksum, and the apk sha1 hash extension.
Source
# File lib/fpm/package/apk.rb, line 368 def hash_record(data) # %u %s=%s\n # len name=hash hash = Digest::SHA1.hexdigest(data) name = "APK-TOOLS.checksum.SHA1" ret = "#{name}=#{hash}\n" # the length requirement needs to know its own length too, because the length # is the entire length of the line, not just the contents. length = ret.length line_length = length.to_s length += line_length.length candidate_ret = "#{line_length} #{ret}" if(candidate_ret.length != length) length += 1 candidate_ret = "#{length.to_s} #{ret}" end ret = candidate_ret # pad out the result ret = pad_string_to(ret, TAR_CHUNK_SIZE) return ret end
SHA-1 hashes the given data, then places it in the APK
hash string format then returns.
Source
# File lib/fpm/package/apk.rb, line 455 def pad_string_to(ret, length) until(ret.length % length == 0) ret << "\0" end return ret end
Appends null zeroes to the end of [ret] until it is divisible by [length]. Returns the padded result.
Source
# File lib/fpm/package/apk.rb, line 161 def register_script(key, value, hash) if(script?(value)) hash[key] = scripts[value] end return hash end
Convenience method for ‘write_control_scripts’ to register control scripts if they exist.
Source
# File lib/fpm/package/apk.rb, line 475 def replace_ownership_headers(header, nullify_names) # magic header[TAR_MAGIC_START..TAR_MAGIC_END] = "ustar\0" + "00" # ids header = replace_string_range(header, TAR_UID_START, TAR_UID_END, "0") header = replace_string_range(header, TAR_GID_START, TAR_GID_END, "0") header[TAR_GID_END] = "\0" header[TAR_UID_END] = "\0" # names if(nullify_names) header = replace_string_range(header, TAR_UNAME_START, TAR_UNAME_END, "\0") header = replace_string_range(header, TAR_GNAME_START, TAR_GNAME_END, "\0") # major/minor header[TAR_MAJOR_START..TAR_MAJOR_END] = "0".rjust(8, '0') header[TAR_MINOR_START..TAR_MINOR_END] = "0".rjust(8, '0') header[TAR_MAJOR_END] = "\0" header[TAR_MINOR_END] = "\0" else header[TAR_UNAME_START..TAR_UNAME_END] = pad_string_to("root", 32) header[TAR_GNAME_START..TAR_GNAME_END] = pad_string_to("root", 32) end return header end
Nulls out the ownership bits of the given tar [header].
Source
# File lib/fpm/package/apk.rb, line 465 def replace_string_range(str, start, finish, character) for i in (start..finish) str[i] = character end return str end
Replaces every character between [start] and [finish] in the given [str] with [character].
Source
# File lib/fpm/package/apk.rb, line 398 def tar_path(path, target_path) # Change directory to the source path, and glob files # This is done so that we end up with a "flat" archive, that doesn't # have any path artifacts from the packager's absolute path. ::Dir::chdir(path) do entries = ::Dir::glob("**", File::FNM_DOTMATCH) args = [ tar_cmd, "-f", target_path, "-c" ] # Move pkginfo to the front, if it exists. for i in (0..entries.length) if(entries[i] == ".PKGINFO") entries[i] = entries[0] entries[0] = ".PKGINFO" break end end # add entries to arguments. entries.each do |entry| unless(entry == '..' || entry == '.') args = args << entry end end safesystem(*args) end end
Tars the current contents of the given [path] to the given [target_path].
Source
# File lib/fpm/package/apk.rb, line 142 def write_control_scripts(base_path) scripts = {} scripts = register_script('post-install', :after_install, scripts) scripts = register_script('pre-install', :before_install, scripts) scripts = register_script('pre-upgrade', :before_upgrade, scripts) scripts = register_script('post-upgrade', :after_upgrade, scripts) scripts = register_script('pre-deinstall', :before_remove, scripts) scripts = register_script('post-deinstall', :after_remove, scripts) scripts.each do |key, content| File.write("#{base_path}/.#{key}", content) end end
Writes each control script from template into the build path, in the folder given by [base_path]
Source
# File lib/fpm/package/apk.rb, line 120 def write_pkginfo(base_path) pkginfo = "" pkginfo << "# Generated by fpm\n" pkginfo << "pkgname = #{@name}\n" pkginfo << "pkgver = #{to_s("FULLVERSION")}\n" pkginfo << "arch = #{architecture()}\n" pkginfo << "pkgdesc = #{description()}\n" pkginfo << "url = #{url()}\n" pkginfo << "size = 102400\n" # totally magic, not sure what it's used for. # write depends lines for dependency in dependencies() pkginfo << "depend = #{dependency}\n" end File.write("#{base_path}/.PKGINFO", pkginfo) end