Skip to content
Snippets Groups Projects
dmg.rb 3.66 KiB
Newer Older
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
require "set"
require "tempfile"

require "hbc/container/base"

module Hbc
  class Container
    class Dmg < Base
      def self.me?(criteria)
        !criteria.command.run("/usr/bin/hdiutil",
                              # realpath is a failsafe against unusual filenames
                              args:         ["imageinfo", Pathname.new(criteria.path).realpath],
                              print_stderr: false).stdout.empty?
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      attr_reader :mounts
      def initialize(*args)
        super(*args)
        @mounts = []
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def extract
        mount!
        assert_mounts_found
        extract_mounts
      ensure
        eject!
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def mount!
        plist = @command.run!("/usr/bin/hdiutil",
                              # realpath is a failsafe against unusual filenames
                              args:  %w[mount -plist -nobrowse -readonly -noidme -mountrandom /tmp] + [Pathname.new(@path).realpath],
                              input: %w[y])
                        .plist
        @mounts = mounts_from_plist(plist)
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def eject!
        @mounts.each do |mount|
          # realpath is a failsafe against unusual filenames
          mountpath = Pathname.new(mount).realpath
          next unless mountpath.exist?

          begin
            tries ||= 2
            @command.run("/usr/sbin/diskutil",
                         args:         ["eject", mountpath],
                         print_stderr: false)

            raise CaskError, "Failed to eject #{mountpath}" if mountpath.exist?
          rescue CaskError => e
            raise e if (tries -= 1).zero?
            sleep 1
            retry
          end
        end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def extract_mounts
        @mounts.each(&method(:extract_mount))
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def extract_mount(mount)
        Tempfile.open(["", ".bom"]) do |bomfile|
          bomfile.close
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

          Tempfile.open(["", ".list"]) do |filelist|
            filelist.write(bom_filelist_from_path(mount))
            filelist.close
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

            @command.run!("/usr/bin/mkbom", args: ["-s", "-i", filelist.path, "--", bomfile.path])
            @command.run!("/usr/bin/ditto", args: ["--bom", bomfile.path, "--", mount, @cask.staged_path])
          end
        end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
      end

      def bom_filelist_from_path(mount)
        Dir.chdir(mount) {
          Dir.glob("**/*", File::FNM_DOTMATCH).map { |path|
            next if skip_path?(Pathname(path))
            path == "." ? path : path.prepend("./")
          }.compact.join("\n").concat("\n")
        }
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def skip_path?(path)
        dmg_metadata?(path) || system_dir_symlink?(path)
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      # unnecessary DMG metadata
      DMG_METADATA_FILES = Set.new %w[
        .background
        .com.apple.timemachine.donotpresent
        .com.apple.timemachine.supported
        .DocumentRevisions-V100
        .DS_Store
        .fseventsd
        .MobileBackups
        .Spotlight-V100
        .TemporaryItems
        .Trashes
        .VolumeIcon.icns
      ].freeze

      def dmg_metadata?(path)
        relative_root = path.sub(%r{/.*}, "")
        DMG_METADATA_FILES.include?(relative_root.basename.to_s)
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def system_dir_symlink?(path)
        # symlinks to system directories (commonly to /Applications)
        path.symlink? && MacOS.system_dir?(path.readlink)
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def mounts_from_plist(plist)
        return [] unless plist.respond_to?(:fetch)
        plist.fetch("system-entities", []).map { |entity|
          entity["mount-point"]
        }.compact
      end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed

      def assert_mounts_found
        raise CaskError, "No mounts found in '#{@path}'; perhaps it is a bad DMG?" if @mounts.empty?
      end
    end
AnastasiaSulyagina's avatar
AnastasiaSulyagina committed
  end
end