lib/linecook/package.rb in linecook-1.2.1 vs lib/linecook/package.rb in linecook-2.0.0

- old
+ new

@@ -1,396 +1,212 @@ -require 'linecook/cookbook' -require 'linecook/recipe' -require 'linecook/template' require 'tempfile' +require 'stringio' module Linecook class Package - class << self - def setup(env, cookbook=nil) - unless env.kind_of?(Hash) - env = Utils.load_config(env) - end - - package = new(env) - - if cookbook - manifest = package.manifest - manifest.replace cookbook.manifest - end - - package - end - - def init(package_file=nil, project_dir=nil) - cookbook = Cookbook.init(project_dir) - package = setup(package_file, cookbook) - - if package_file - package.context[PACKAGE_KEY] ||= begin - name = File.basename(package_file).chomp(File.extname(package_file)) - {'recipes' => { 'run' => name }} - end - end - - package - end - end - - CONTEXT_KEY = 'linecook' - COOKBOOK_KEY = 'cookbook' - MANIFEST_KEY = 'manifest' - PACKAGE_KEY = 'package' - REGISTRY_KEY = 'registry' - - FILES_KEY = 'files' - TEMPLATES_KEY = 'templates' - RECIPES_KEY = 'recipes' - # The package environment attr_reader :env - - # An array of tempfiles generated by self (used to cleanup on close) - attr_reader :tempfiles - - # A hash of counters used by variable. - attr_reader :counters - - # A hash of callbacks registered with self - attr_reader :callbacks - + + # A registry of (path, source_path) pairs recording what files are + # included in the package. + attr_reader :registry + + # A hash of (path, Hash) pairs identifing export options for a package + # file. See on_export. + attr_reader :export_options + + # A hash of default export options. + attr_reader :default_export_options + def initialize(env={}) @env = env - @tempfiles = [] - @counters = Hash.new(0) - @callbacks = Hash.new {|hash, key| hash[key] = StringIO.new } + @registry = {} + @export_options = {} + @default_export_options = {} end - - # Returns the linecook context in env, as keyed by CONTEXT_KEY. Defaults - # to an empty hash. - def context - env[CONTEXT_KEY] ||= {} + + # Registers a file into the package with the specified export options. The + # source path should be the path to a file or directory to include. To + # make an empty file or directory use :file or :dir as the source_path. + # + # Raises an error if a source is already registered at the path. + def register(path, source_path, options={}) + package_path = path.to_s.strip + + if package_path.empty? + raise "invalid package path: #{path.inspect}" + end + + if registry.has_key?(package_path) + raise "already registered: #{path.inspect}" + end + + source_path = resolve_source_path(source_path) + registry[package_path] = source_path + on_export(package_path, options) + + source_path end - - # Returns the manifest in config, as keyed by MANIFEST_KEY. Defaults to an - # empty hash. - def manifest - context[MANIFEST_KEY] ||= {} + + # Removes a file from the package. Returns the source path if one was + # registered. + def unregister(path) + registry.delete(path) + export_options.delete(path) end - - # Returns the registry in config, as keyed by REGISTRY_KEY. Defaults to an - # empty hash. A hash of (target_name, source_path) pairs identifying - # files that should be included in a package - def registry - context[REGISTRY_KEY] ||= {} - end - - # Returns the linecook configs in env, as keyed by CONFIG_KEY. Defaults - # to an empty hash. - def config - context[PACKAGE_KEY] ||= {} - end - - # Returns the hash of (source, target) pairs identifying which of the - # files will be built into self by build. Files are identified by - # FILES_KEY in config, and normalized the same way as recipes. - def files - config[FILES_KEY] = Utils.hashify(config[FILES_KEY]) - end - - # Returns the hash of (source, target) pairs identifying which templates - # will be built into self by build. Templates are identified by - # TEMPLATES_KEY in config, and normalized the same way as recipes. - def templates - config[TEMPLATES_KEY] = Utils.hashify(config[TEMPLATES_KEY]) - end - - # Returns the hash of (source, target) pairs identifying which recipes - # will be built into self by build. Recipes are identified by RECIPES_KEY - # in config. + + # Sets export options for the package file. Available options (as + # symbols): # - # Non-hash recipes are normalized by expanding arrays into a redundant - # hash, such that each entry has the same source and target (more - # concretely, the 'example' recipe is registered as the 'example' script). - # Strings are split along colons into an array and then expanded. + # move:: When set to true the source will be moved into place + # rather than copied (the default) + # mode:: Sets the mode of the package file # - # For example: - # - # package = Package.new('linecook' => {'package' => {'recipes' => 'a:b:c'}}) - # package.recipes # => {'a' => 'a', 'b' => 'b', 'c' => 'c'} - # - def recipes - config[RECIPES_KEY] = Utils.hashify(config[RECIPES_KEY]) + # Unless specified, the values in default_export_options will be used. + def on_export(path, options={}) + export_options[path] = default_export_options.merge(options) end - - # Registers the source_path to target_name in the registry and - # revese_registry. Raises an error if the source_path is already + + # Generates a tempfile and registers it into the package at the specified + # path. Returns the open tempfile. + def add(path, options={}) + options = { + :move => true + }.merge(options) + + # preserve a reference to tempfile in options so that it will not be + # unlinked before it can be moved into the package during export + tempfile = Tempfile.new File.basename(path) + options[:tempfile] = tempfile + + if block_given? + begin + yield tempfile + ensure + tempfile.close + end + end + + register path, tempfile.path, options + tempfile + end + + # Adds an empty dir at path. Returns nil. + def add_dir(path, options={}) + register path, :dir, options + end + + alias rm unregister + + # Returns the source path registered at the path, or nil if no source is # registered. - def register(target_name, source_path, mode=0600) - source_path = File.expand_path(source_path) - - if registry.has_key?(target_name) && registry[target_name] != [source_path, mode] - raise "already registered: #{target_name} (%s, %o)" % registry[target_name] + def source_path(path) + registry[path] + end + + # Returns an array of paths that the source path is registered to. + def paths(source_path) + source = resolve_source_path(source_path) + + paths = [] + registry.each_pair do |path, current| + if current == source + paths << path + end end - - registry[target_name] = [source_path, mode] - target_name + paths end - - # Increments target_name until an unregistered name is found and returns - # the result. - def next_target_name(target_name='file') + + # Returns the content to be added to the package at the path. Returns nil + # if nothing is registered. + def content(path, length=nil, offset=nil) + source = source_path(path) + source ? File.read(source, length, offset) : nil + end + + # Increments path until an unregistered path is found and returns the + # result in the format "path.count". + def next_path(path='file') count = 0 - registry.each_key do |key| - if key.index(target_name) == 0 + registry.each_key do |current| + if current.index(path) == 0 count += 1 end end - + if count > 0 - target_name = "#{target_name}.#{count}" + path = "#{path}.#{count}" end - - target_name + + path end - - # Returns a package-unique variable with base 'name'. - def next_variable_name(context) - context = context.to_s - - count = counters[context] - counters[context] += 1 - - "#{context}#{count}" - end - - # Returns true if there is a path for the specified resource in manifest. - def resource?(type, path) - resources = manifest[type] - resources && resources.has_key?(path) - end - - # Returns the path to the resource in manfiest. Raises an error if there - # is no such resource. - def resource_path(type, path) - resources = manifest[type] - resource_path = resources ? resources[path] : nil - resource_path or raise "no such resource in manifest: #{type.inspect} #{path.inspect}" - end - - # Returns the resource_path the named attributes file (ex 'attributes/name.rb'). - def attributes_path(attributes_name) - resource_path('attributes', attributes_name) - end - - # Returns the resource_path the named file (ex 'files/name') - def file_path(file_name) - resource_path('files', file_name) - end - - # Returns the resource_path the named template file (ex 'templates/name.erb'). - def template_path(template_name) - resource_path('templates', template_name) - end - - # Returns the resource_path the named recipe file (ex 'recipes/name.rb'). - def recipe_path(recipe_name) - resource_path('recipes', recipe_name) - end - - # Loads the named attributes file into and returns an instance of - # Attributes. The loading mechanism depends on the attributes file - # extname. - # - # .rb: evaluate in the context of attributes - # .yml,.yaml,.json: load as YAML and merge into attributes - # - # All other file types raise an error. Simply returns a new Attributes - # instance if no name is given. - def load_attributes(attributes_name=nil) - attributes = Attributes.new - - if attributes_name - path = attributes_path(attributes_name) - - case File.extname(path) - when '.rb' - attributes.instance_eval(File.read(path), path) - when '.yml', '.yaml', '.json' - attributes.attrs.merge!(YAML.load_file(path)) - else - raise "invalid attributes format: #{path.inspect}" + + def export(dir) + registry.keys.sort.each do |path| + target_path = File.join(dir, path) + source_path = registry[path] + options = export_options[path] || default_export_options + + if source_path != target_path + if File.exists?(target_path) + if block_given? + unless yield(source_path, target_path) + next + end + else + raise "already exists: #{target_path.inspect}" + end + end + + target_dir = File.dirname(target_path) + FileUtils.mkdir_p(target_dir) + + case source_path + when :file + FileUtils.touch target_path + when :dir + FileUtils.mkdir target_path + else + if File.directory?(source_path) + export_dir(source_path, target_path, options) + else + export_file(source_path, target_path, options) + end + end end + + if mode = options[:mode] + FileUtils.chmod(mode, target_path) + end + + registry[path] = target_path end - - attributes + + registry end - - # Load the template file with the specified name and wraps as a Template. - # Returns the new Template object. - def load_template(template_name) - Template.new template_path(template_name) - end - - # Loads and returns the helper constant specified by helper_name. The - # helper_name is underscored to determine a require path and camelized to - # determine the constant name. - def load_helper(helper_name) - require Utils.underscore(helper_name) - Utils.constantize(helper_name) - end - - # Returns a recipe bound to self. - def setup_recipe(target_name = next_target_name, mode=0700) - Recipe.new(self, target_name, mode) - end - - # Generates a tempfile for the target path and registers it with self. As - # with register, the target_name will be incremented as needed. Returns - # the open tempfile. - def setup_tempfile(target_name = next_target_name, mode=0600) - tempfile = Tempfile.new File.basename(target_name) - - register(target_name, tempfile.path, mode) - tempfiles << tempfile - - tempfile - end - - # Returns true if the source_path is for a tempfile generated by self. - def tempfile?(source_path) - tempfiles.any? {|tempfile| tempfile.path == source_path } - end - - # Looks up the file with the specified name using file_path and registers - # it to target_name. Raises an error if the target is already registered. - def build_file(target_name, file_name=target_name, mode=0600) - register target_name, file_path(file_name), mode - self - end - - # Looks up the template with the specified name using template_path, - # builds, and registers it to target_name. The locals will be set for - # access in the template context. Raises an error if the target is - # already registered. Returns self. - def build_template(target_name, template_name=target_name, mode=0600, locals={'attrs' => env}) - content = load_template(template_name).build(locals) - - target = setup_tempfile(target_name, mode) - target << content - target.close - self - end - - # Looks up the recipe with the specified name using recipe_path, evaluates - # it, and registers the result to target_name. Raises an error if the - # target is already registered. Returns self. - def build_recipe(target_name, recipe_name=target_name, mode=0700) - path = recipe_path(recipe_name) - recipe = setup_recipe(target_name, mode) - recipe.instance_eval(File.read(path), path) - recipe.close - - self - end - - # Builds the files, templates, and recipes for self. Returns self. - def build - files.each do |target_name, file_name| - build_file(target_name, *file_name) + + private + + def resolve_source_path(source_path) # :nodoc: + case source_path + when :file, :dir then source_path + else File.expand_path(source_path.to_s) end - - templates.each do |target_name, template_name| - build_template(target_name, *template_name) - end - - recipes.each do |target_name, recipe_name| - build_recipe(target_name, *recipe_name) - end - - self end - - # Returns the content of the source_path for target_name, as registered in - # self. Returns nil if the target is not registered. - def content(target_name, length=nil, offset=nil) - path = source_path(target_name) - path ? File.read(path, length, offset) : nil - end - - # Returns the source_path for target_name, as registered in self. Returns - # nil if the target is not registered. - def source_path(target_name) - entry = registry[target_name] - entry ? entry[0] : nil - end - - # Returns the mode for target_name, as registered in self. Returns nil if - # the target is not registered. - def mode(target_name) - entry = registry[target_name] - entry ? entry[1] : nil - end - - # Closes all tempfiles and returns self. - def close - tempfiles.each do |tempfile| - tempfile.close unless tempfile.closed? + + def export_dir(source_path, target_path, options) # :nodoc: + if options[:move] + FileUtils.mv(source_path, target_path) + else + FileUtils.cp_r(source_path, target_path) end - self end - - # Closes and clears all tempfiles, the registry, callbacks, and counters. - def reset - close - registry.clear - tempfiles.clear - callbacks.clear - counters.clear - self - end - - # Closes self and exports the registry to dir by copying or moving the - # registered source paths to the target path under dir. By default - # tempfiles are moved while all other files are copied. - # - # Returns registry, which is re-written to reflect the new source paths. - def export(dir, options={}) - close - - options = { - :allow_move => true - }.merge(options) - - allow_move = options[:allow_move] - - if File.exists?(dir) - FileUtils.rm_r(dir) + + def export_file(source_path, target_path, options) # :nodoc: + if options[:move] + FileUtils.mv(source_path, target_path) + else + FileUtils.cp(source_path, target_path) end - - registry.each_key do |target_name| - export_path = File.join(dir, target_name) - export_dir = File.dirname(export_path) - source_path, mode = registry[target_name] - - next if source_path == export_path - - unless File.exists?(export_dir) - FileUtils.mkdir_p(export_dir) - end - - if allow_move && tempfile?(source_path) - FileUtils.mv(source_path, export_path) - else - FileUtils.cp(source_path, export_path) - end - - FileUtils.chmod(mode, export_path) - - registry[target_name] = [export_path, mode] - end - - tempfiles.clear - registry end end end \ No newline at end of file