# = TITLE:
#
# Project
#
# = COPYING:
#
# Copyright (c) 2007 Tiger Ops
#
# This file is part of the ProUtils' Ratch program.
#
# Ratch is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ratch is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ratch. If not, see .
require 'facets/dir/multiglob'
require 'reap/iobject'
module Reap
class Project
# = Project Metadata
#
# The Project Metadata class stores project information. This information includes
# the general information about a project, such as title, description, homepage, etc.
# which is essentially static. Once set, it will probably will never change.
# The class also contains default settings for packaging; information that is
# usually static, but may vary for a partciular package platform or format.
#
# When utilizing this class it is important not confuse oneself thinking that a
# project is not a project just becuase it is a sub-project. A sub-project is a
# project, it just happens to belong to a master project.
class Metadata < InfoObject
PROJECT_FILE = '{,meta/}{project}{info,}{.yaml,.yml,}'
VERSION_FILE = '{,meta/}{version}{.text,.txt,}'
#
def self.read(location)
metadata = read_project(location)
versdata = read_version(location)
data = {}
data.update(metadata)
data.update(versdata)
new(location, data)
end
# Parse release file for release information.
def self.read_project(location)
glob = File.join(location, PROJECT_FILE)
file = Dir.glob(glob, File::FNM_CASEFOLD).first
if file
YAML::load(File.open(file))
else
raise LoadError, "project file not found"
end
end
# Parse version file for current release information.
def self.read_version(location)
glob = File.join(location, VERSION_FILE)
file = Dir.glob(glob, File::FNM_CASEFOLD).first
if file
str = File.read(file)
version, status, date, *null = *str.strip.split(/\s+/)
date = Date.parse(date).strftime("%Y-%m-%d")
data = {'version' => version, 'status' => status, 'date' => date}
else
data = {}
end
return data
end
# New Project.
def initialize(location, data={})
@location = location
super(data)
end
# Location is needed to calculate some conventional defaults.
attr_accessor :location
# General
#------------------------------------------------------------------------
# The title of the project (free-form, defaults to name).
attr_accessor :title do
@title || (
name if respond_to?(:name)
)
end
# Subtitle is limited to 60 characters.
attr_accessor :subtitle do
@subtitle.to_s[0..59]
end
# Brief one-line description of the package (Max 80 chars.)
attr_accessor :summary, :brief do
if @summary
@summary.to_s[0..79]
else
i = @description.index('.') || 79
i = 79 if i > 79
@description[0..i]
end
end
# More detailed description of the package.
attr_accessor :description, :synopsis
# "Unix" name of this project.
attr_accessor :project, :name
# Overrides Project#name.
# TODO: Fit release name into name or package_name (?)
#def name
# @name ||= release.name
#end
# If this is a sub-project, then +master+ is the
# "Unix" name of the master project to which this
# sub-project belongs.
attr_accessor :master
# The date the project was started.
attr_accessor :created
# Copyright notice.
attr_accessor :copyright do
@copyright || "Copyright (c) #{Time.now.strftime('%Y')} #{author}"
end
# Distribution License.
attr_accessor :license do
@license || 'GPLv3'
end
# Slogan or "trademark" phrase.
attr_accessor :slogan
# General one-word software category.
attr_accessor :category
# Author(s) of this project.
# (Usually in "name " format.)
attr_accessor :author
# Contact(s) (defaults to authors).
# TODO Move to Variants?
attr_accessor :contact do
@contact || author
end
# Gerneral email address.
attr_accessor :email do
if md = /<(.*?)>/.match(contact)
md[1]
else
"ruby-talk@ruby-lang.org"
end
end
# Official domain associated with this package.
attr_accessor :domain
# Project's homepage.
attr_accessor :homepage, :website
# Project's development site.
attr_accessor :development, :devsite
# Internet address(es) to online documentation.
attr_accessor :documentation, :docs
# Internet address(es) to downloadable packages.
attr_accessor :download
# Internet address for project wiki.
attr_accessor :wiki
# Project's mailing list.
attr_accessor :userlist, :mailinglist, :list
# Developer's mailing list.
attr_accessor :devlist do
@devlist || @userlist
end
# Returns a standard taguri id for the library and release.
def project_taguri
"tag:#{name}.#{domain},#{created}" # or released?
end
# Version
#------------------------------------------------------------------------
# Version number (eg. '1.0.0').
attr_accessor :version
# Current version code name.
attr_accessor :codename
# Build number can br set to an arbitrar number, or if set to true,
# it will defaults to a number based on current date-time.
attr_accessor :buildno do
@buildno = Time.now.strftime("%y%m%d%H%M") if TrueClass === @buildno
@buildno
end
# Status of this release: alpha, beta, RC1, etc.
attr_accessor :status do
@status || 'alpha'
end
# Date of release.
attr_accessor :date, :released do
@date
end
# Content Classification
#------------------------------------------------------------------------
# Files in this package that are executables.
# These files must in the packages bin/ directory.
# If left blank all bin/ files are included.
attr_accessor :executable, :executables do
return [@executable].flatten.compact if @executable
exes = []
dir = File.join(location, 'bin')
if File.directory?(dir)
Dir.chdir(dir) do
exes = Dir.glob('*')
end
end
@executable = exes
end
# Library files in this package that are *public*.
# This is akin to load_path but specifies specific files
# that can be loaded from the outside --where as those
# not listed are considerd *private*.
#
# NOTE: This is not enforced --and may never be. It
# complicates library loading. Ie. how to distinguish public
# loading from external loading. But it something that can be
# consider more carfully in the future. For now it can serve
# as an optional reference.
attr_accessor :library, :libraries do
[@library || 'lib/**/*'].flatten
end
# Location(s) of executables.
attr_accessor :bin_path, :bin_paths, :binpath, :binpaths
# Location(s) of libraries (used by Rolls).
# In most cases this is something like:
#
# 'lib/myapp'
#
# It would be nice if this could just be lib/ as it would mean one less
# layer in a project heirarchy. But RubyGems and traditional installers
# could not handle this, so this isn't a reasonable course at this point.
attr_accessor :lib_path, :lib_paths, :libpath, :libpaths do
[@lib_path || "lib/#{name}"].flatten
end
# The traditional load path(s) (used by Ruby's own site loading and RubyGems).
# The default is lib/, which is usually correct.
attr_accessor :load_path, :load_paths, :loadpath, :loadpaths, :gem_path, :gem_paths, :gempath, :gempaths do
[@load_path || "lib"].flatten
end
# This only applys to Rolls. It is the default file to load.
# TODO: Think of a more descirptive name than 'default'.
attr_accessor :default
# Security
#------------------------------------------------------------------------
# Encryption digest type used.
# (md5, sha1, sha128, sha256, sha512).
attr_accessor :digest do
@digest || 'md5'
end
# Public key file associated with this library. This is useful
# for security purposes especially remote loading. [pubkey.pem]
attr_accessor :public_key do
@public_key || 'pubkey.pem'
end
# Private key file associated with this library. This is useful
# for security purposes especially remote loading. [_privkey.pem]
attr_accessor :private_key
# @private_key || '_privkey.pem'
# end
# Source Management
#------------------------------------------------------------------------
# Specify which verison control system is being used.
# Sometimes this is autmatically detectable, but it
# is better to specify it.
# Specifices the type of revision control system used.
# darcs, svn, cvs, etc.
# Will try to determine which version control system is being used.
attr_accessor :scm do
return @scm unless @scm.nil?
@scm = if File.directory?('.svn')
'svn'
elsif File.directory?('_darcs')
'darcs'
else
false
end
end
# Files that are tracked under revision control.
# Default is all less standard exceptions.
# '+' and '-' prefixes can be used to augment the list
# rather than fully override it.
attr_accessor :track, :scm_files
# Internet address to source code repository.
# (http://, ftp://, etc.)
attr_accessor :repository, :repo
# Changelog file.
attr_accessor :changelog
# Manifest file. Defaults to 'MANIFEST'.
# (I like to put it in meta/MANIFEST, personally.)
attr_accessor :manifest do
@manifest ||= 'MANIFEST'
end
# Dependencies
#------------------------------------------------------------------------
# Package inter-relationship data. Generally refered to as package
# "dependencies", but also includes +recommendations+, +suggestions+,
# +replacements+, +provisions+, and +build-dependencies+, as well
# as a few other fields that set a package apart.
#------------------------------------------------------------------------
# What other packages *must* this package have in order to function.
attr_accessor :dependency, :dependencies do
@dependency || []
end
# What other packages *should* be used with this package.
attr_accessor :recommend, :recommends, :recommendations do
@recommend || []
end
# What other packages *could* be useful with this package.
attr_accessor :suggest, :suggests, :suggestions do
@suggest || []
end
# What other packages does this package conflict.
attr_accessor :conflict, :conflicts do
@conflict || []
end
# What other packages does this package replace.
attr_accessor :replace, :replaces, :replacements do
@replace || []
end
# What other package(s) does this package provide the same dependency fulfilment.
# For example, a package 'bar-plus' might fulfill the same dependency criteria
# as package 'bar', so 'bar-plus' is said to provide 'bar'.
attr_accessor :provide, :provides, :provisions do
@provide || []
end
# Abirtary information about what might be needed to use this package.
# This is strictly information for the end-user to consider.
# Eg. "Fast graphics card"
attr_accessor :requirement, :requirements do
@requirement || []
end
# What packages does this package need to build? (eg. 'rake', 'reap', etc.)
attr_accessor :build_dependency, :build_dependencies do
@build_dependency || []
end
# Abirtary information about what might be needed to build this package.
attr_accessor :build_requirement, :build_requirements do
@build_requirement || []
end
# Packaging
#------------------------------------------------------------------------
# Package name. This defaults to project name, but it may vary under
# different package formats --deb vs. gem, for instance.
attr_accessor :package do
@package || name
#@package || (
# name if respond_to?(:name)
#)
end
# Platform. The default is nil, which is considered cross-platform.
# This tends to only change for special builds.
#
# TODO: if current?
attr_accessor :platform
# Architecture(s) this package can be run on: any, i386, i686, ppc, etc.
# This is strictly informational and is inteded to indicate the possiblities,
# not the particular platform this package runs on.
#attr_accessor :arch, :architecture do
# @arch || "any"
#end
# Script to run prior to build. No entry indicates no compilation.
attr_accessor :compile
# Packages that are intended to compile on install may need this. It is a list
# of "extension scripts" which generate Makefiles for use in compilation.
attr_accessor :extensions do
[@extensions || Dir.glob(File.join(location, 'ext/**/extconf.rb'))].flatten.compact
end
#
#validate "compile script not found" do
# compile ? File.file?(compile) : true
#end
# Generate documentation on installation?
attr_accessor :document, :has_rdoc
#
#attr_accessor :package_directory, :package_store do
# @package_directory || 'pkg'
#end
# Package name is generally in the form of +name-version+, or
# +name-version-platform+ if +platform+ is specified.
#
# TODO: Improve buildno support.
def package_name
if buildno
buildno = Time.now.strftime("%H*60+%M")
versnum = "#{version}.#{buildno}"
else
versnum = version
end
if platform
"#{package}-#{versnum}-#{platform}"
else
"#{package}-#{versnum}"
end
end
alias_method :stage_name, :package_name
# Distribution
#------------------------------------------------------------------------
# Files to be distributed in a package. Defaults to all files.
# If an entry is a directory then all it's contents are also included.
# This along with @exclude@ and @ignore@ is used to generate a manifest.
attr_accessor :distribute, :include do
[@distribute || '**/*'].flatten.compact
end
# File to exclude from package. This is usually more useful than
# @distribute@, as it allows you to remove from all files, rather then
# explicitly designate everything to be included. Exlcusions have priority
# over dsitribute's inclusions. If an entry is a directory then all
# it's contents are also excluded.
attr_accessor :exclude do
[@exclude].flatten.compact
end
# Files to generally ignore, mainly used for manifest collection. Ignore
# has priority over @exclude@ and @distribute@.
attr_accessor :ignore do
@ignore || %w{ **/.svn _darcs .config .installed }
end
# Manifest file.
#def manifest
# @manifest #||= Manifest.open
#end
# Set manifest file, which will load it.
#def manifest=(file)
# @manifest = file
# @filelist = File.read_list(file) #Manifest.open(file)
# return file
#end
# List of file included in a package. This is generated using
# @distribute@, @exlude@ and @ignore@.
def filelist
@filelist ||= collect_files(true)
end
# Validate that the files in the manifest actually exist.
#def validate_manifest
# missing = []
# filelist.each do |f|
# missing << f unless File.exist?(f)
# end
# unless missing.empty?
# raise ValidationError, "manifest lists non-existent files -- " + missing.join(" ")
# end
#end
# Access to binding for use with ERB.
def get_binding
binding
end
private
# Collect distribution files.
def collect_files(with_dirs=false)
files = []
Dir.chdir(location) do
files += Dir.multiglob_r(*distribute)
files -= Dir.multiglob_r(*exclude)
files -= Dir.multiglob_r(*ignore)
files -= Dir.multiglob_r('pkg') #package_directory
end
# Do not include symlinks.
files.reject!{ |f| FileTest.symlink?(f) }
unless with_dirs
files = files.select{ |f| !File.directory?(f) }
end
return files
end
# Validation
#------------------------------------------------------------------------
public
#
validate "version is required" do
version
end
#
validate "location is required" do
location
end
#
validate "executables do not exist" do
exes = []
dir = File.join(location, 'bin')
if File.directory?(dir)
Dir.chdir(dir) do
exes = Dir.glob('*')
end
end
(executables - exes).empty?
end
end
end
end