#!/usr/bin/env python
'''Creates an RPM spec file from a project makefile or file list

Copyright 1998-2003 Daniel Fandrich <dan@coneharvesters.com>

$Id: autospec.py,v 1.22 2003/04/01 18:27:27 dan Exp $
'''

# Bugs:
#  - if the name ends in digits (e.g. Alec64) the digits are interpreted as 
#    the version number
#  - if the version contains letters (e.g. foo-0.x.5) the letter is not
#    included in the version (this is a pathological case, but one I've seen).
#
# To do:
#  - append * to man pages and info files in %files list
#     - careful about info files because foo.info* also matches foo.info.1*
#  - sort files in %files so man, bin, etc. files stay together
#  - look for icon-sized XPM images and put them in an Icon: line
#  - fix finddir to do substring search for directory
#  - add flag to create -devel section and put the obvious devel stuff in there
#  - when reading README files, uncompress them first if necessary
#  - use contents (1st line) of a file called VERSION as the version number
#    if it can't be determined otherwise
#  - Parse automake's Makefile.am files to get list of files to use in autospec
#  - Add feature to grab all information it can from an RPM already in
#    the RPM database.
#    - use --queryformat to put it into an easy-to-parse format (LSM?).
#  - mention similar projects
#	 - http://rpmbuilder.sourceforge.net/
#	 - http://rpmrebuild.sourceforge.net
#	 - alien
#	 - checkinstall
#  - When substituting directory macros, make sure they're compatible with
#    the current distribution
#  - Maybe convert /usr/local/* to standard macros if we have control over
#    the install process, or if using autoconf
#  - in %install, install several files at once into the same directory 
#  - for rpm ver. 2.4, could put lots of %define _sbindir et. al. at the 
#    head of the file since all the macros seem to work with them there
#  - if GNU configure is detected, enable BuildRoot
#  - if setup.py is found, do distutils 'python setup.py install' install &
#    use Python distutils metadata instead of LSM, if available (PKG-INFO file?)
#    - this might not be worthwhile; if setup.py exists, then you can just
#      generate an RPM with bdist_rpm
#    - if I do use this, run it in a restricted environment to avoid trojans
#  - Put a modification of the current detected distribution into Distribution:
#    e.g. "Custom package for Conectiva Linux"
#  - split man page up into sections for selecting the format of the files
#    (-w, -i), the type of source archive (-b, -z) and information to go
#    straight into the spec file (-g, -c)
#  - add directory prefix option for -b for relative binary tgzs (e.g. maxwell)
#  - improve command line parsing, especially quotes and redirection
#  - support multiple files for chmod, touch, etc.; maybe invoke
#	 self.multiplefiles for each, but don't overwrite existing entries
#  - maybe catch all redirections, not just those for echo
#  - maybe treat || and && separators as semicolons
#  - in the case of file globbing in cp or chmod -R commands,
#    actually look at files on disk to see what's there
#  - warn about subshell in ( ) but do the commands inside anyway; tricky:
#    ReadContLines only passes the opening parenthesis if a semicolon is in line
#    - maybe just blindly strip off leading ( in tokens[0][0] and trailing ) 
#      in tokens[-1][-1]
#  - maybe handle for loops where the whole loop is on a single line
#  - maybe handle if [ xxx ] ; then ... enough for xmkmf makefiles
#  - detect Imakefile and put xmkmf commands in
#    (%build; xmkmf; %install make install-man)
#  - add option that takes files on stdin and creates an install script
#    that guesses where the non-absolute files go, either /usr/bin,
#    /usr/X11R6/bin, /usr/lib/{rpmname}, /usr/man/manX/
#    - guess based on file modes, new patterns, `ldd' dependencies, `file' type
#  - look at '/etc/X11/\(wmconfig|applnk\)' file to guess at a Group:
#  - look at contents of all files and enable 'BuildArchitectures: noarch'
#    if applicable
#  - look for documentation file named 'foo.txt' where foo is the RPM name
#  - support bzip2 source archives (isn't this free in new RPMs?)
# 
# $Log: autospec.py,v $
# Revision 1.22  2003/04/01 18:27:27  dan
# Fixed a bug that tried to copy null files into directories in %install
#
# Revision 1.21  2003/04/01 17:56:42  dan
# Bumped version to 0.8
#
# Revision 1.20  2003/03/09 07:57:49  dan
# Fixed misspelled post install/uninstall script sections.
# Quoted some more buildroot variables.
# Added --sysconfdir=/etc to default ./configure command.
#
# Revision 1.19  2003/02/14 16:18:46  dan
# Ignore warnings on regsub module, too.
#
# Revision 1.18  2003/01/22 06:54:43  dan
# Bumped version number to 0.7.2
# Shut up Python 2 when it imports the deprecated regex module.
#
# Revision 1.17  2002/10/04 06:37:54  dan
# Split off ParseCommands and subclasses into parsecommands.py
#
# Revision 1.16  2002/10/04 04:16:53  dan
# Use getentry and setattrib to clean code and de-dupe file entries.
# Treat seperator lines in README files like blanks.
# Change macro file name substitution so longer paths take precedence.
#
# Revision 1.15  2002/09/26 07:20:25  dan
# Substitute %{version} for version in source file name.
# Use directory and program macros for file paths for rpm ver. >= 3.0
# Don't attempt to read binary README files.
#
# Revision 1.14  2002/09/20 06:30:43  dan
# guessgroup is now a method on FileListAnalyze.
#
# Revision 1.13  2002/09/19 04:54:10  dan
# Added buildarch to Spec class, and set it to 'noarch' on Documentation
# packages.
# Minor usage text changes.
#
# Revision 1.12  2002/05/14 05:26:42  dan
# Added some rpm ver. 3 features, and produce ver. 3 spec files by default.
# Checking contents of some doc files to try to guess the license.
# Generate %configure line only when configure file is found in current dir.
# Rearchitected FileInfo and file guessing functions and moved to fileanalyze.py
# Split off handleinstallline from parseinstall.
# Warn about more shell features we don't support.
# Split off writespec from install2spec.
# Added %postinstall section for libraries.
# Guess at the Group: name based on the file list.
# Use the first part of a README file as a description if no LSM is available.
#
# Revision 1.11  2002/02/10 01:24:03  dan
# Added exit code 2 on parse error.
# Split command-line parsing into ParseCommands classes.
# Added support for installwatch logs (-w option).
# Added multilingual support (-l option).
# Added more documentation files.
#
# Revision 1.10  2001/05/08 20:45:13  dan
# Added -z option for ZIP archives.  Added %clean section.  Added quoting
# of file names with special characters.  Fixed bug in BuildRoot name.
# Eliminated other file flags if %dir is present.
#
# Revision 1.9  2001/03/30 04:51:45  dan
# Added -r RPM version compatibility option.  Improved install script parsing
# and fixed exception that occurred with some input.  Added more magic
# documentation file names.  Using BuildRoot whenever -i not used. Added
# %changelog section. Created options class.  Upped version number to 0.5
#
# Revision 1.8  2000/06/05 19:51:57  dan
# Improved comand-line parsing by at least partially supporting quoted args.
#
# Revision 1.7  1999/03/27 20:09:17  dan
# Change to use new splitfirst semantics. Improve chown processing.
# Add findurl to look for URLs in the description field of the LSM file.
# Add install commands to the %install section if -i is not given.
# Upped version number to 0.3.1
#
# Revision 1.6  1999/02/26 08:12:47  dan
# Added ranlib to install ignore list. Upped version number to 0.3
#
# Revision 1.5  1999/02/26 07:15:10  dan
# Now using posixpath to handle file names. Renamed -m to -i.
# Added -m option to take information from LSM file.  Reversed meaning of -d.
# Added entries to doc name list. Added bzip2 to install ignore list.
#
# Revision 1.4  1998/08/06 04:20:07  dan
# Added -c and -g options. Upped version number to 0.2.
# 
# Revision 1.3  1998/07/20 20:47:42  dan
# Added -b and -n options.
# 
# Revision 1.2  1998/07/14 23:00:20  dan
# Added my name and e-mail address.
# 
# Revision 1.1  1998/07/14 22:52:16  dan
# Initial revision
#

license = '''\
   Copyright 1997-2003 by Daniel Fandrich <dan@coneharvesters.com>
   This program 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 2, or (at your option)
   any later version.

   This program 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 this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
'''

usage_msg = '''\
Automatically creates an RPM spec file with files given on stdin

usage: %s [-bdhimL] [-c copyright] [-g groupname] [-l language_list]
          [-n name-version] [-r rpm-version]
 -b         package source contains binaries in the proper directories
 -c         specifies copyright restrictions (Copyright: or License:)
 -d         don't search for extra documentation files in current directory
 -g         specifies the classification group for the package (Group:)
 -h         show this help
 -i         install script (e.g. 'make -n install') appears on stdin, else list
 -l         prepare for translations in this comma-separated list of languages
 -m         don't use a Linux Software Map (LSM) file if found in current dir
 -n         RPM name and optional version (Name: and Version:)
 -r         produce spec file compatible with this RPM version
 -w         output from installwatch appears on stdin 
 -z         source is in a ZIP archive instead of a gzipped tar ball
 -L         display software license
'''

version_msg = 'autospec ver. 0.8'

try:
	# Shut up Python ver. > 2 when we use the regex module
	import warnings
	warnings.filterwarnings('ignore', '^the regex module', DeprecationWarning)
	warnings.filterwarnings('ignore', '^the regsub module', DeprecationWarning)
except ImportError:
	pass
from string import strip, split, join, find, atoi, atof, lower, letters, digits
from os import error, path, getcwd, listdir, getuid, uname
from urlparse import urlparse
from pwd import getpwuid
from fileanalyze import FileInfo,FileListAnalyze,lookforlicense
from parsecommands import ParseCommandLine,ParseInstallwatch
import classifyfile
import commands
import time
import getopt
import sys
import posixpath
import regex
import regsub
from danutil import printerr,splitfirst,quotedsplit,span
from lsm import CanonicalLSM,fileurl,joinstrings,BadSite
try:
	import locale
except ImportError:
	# Python <1.5 does not have locale
	pass

OSError = error		# "error" is too generic, so rename and get rid of it
del error

defaultmode = 0644		# default file mode
defaultexemode = 0755	# default file mode for executables
defaultowner = 'root'	# default file owner
defaultownernum = '0'	# default file owner (numerical)
defaultgroup = 'root'	# default file group
defaultgroupnum = '0'	# default file group (numerical)

DISTRIBUTION = 'Red Hat Contrib-Net'

#debuglog = printerr			# enable this for debug logs
def debuglog(errstr): pass		# enable this to stop debug logs

class Options:
	'Class containing all the spec file generation options'
	docfilepath = '.'
	rpmnamever = None
	group = None
	copyright = None
	binarymode = 0
	zipmode = 0
	uselsm = 1
	rpmver = 3.0
	languages = []

	# Possible sources of file names
	FILES_NONE = 0
	FILES_INSTALL = 1
	FILES_LIST = 2
	FILES_INSTALLWATCH = 3
	filessource = FILES_NONE


class ReadContLines:
	'''Class to read command lines and split semicolons and concatenate
	'\' continuation lines'''

	def __init__(self, file):
		self.file = file
		self.lineremaining = ''

	def readline(self):
		'Read the next command from file'
		if self.lineremaining:
			# Process the remainder of a line after a semicolon
			line = self.lineremaining
			self.lineremaining = ''
		else:
			line = self.file.readline()
			if not line:
				# End of file
				return line

		# Concatenate lines ending in \
		while line[-2:] == '\\\n':
			newline = self.file.readline()
			if not newline:
				break
			line = line[:-2] + newline

		# Break line up at the ; command separator
		# This is very broken as it ignores quotes and escape sequences
		try:
			line,self.lineremaining = splitfirst(line,';')
			debuglog('Remaining line: '+`self.lineremaining`)
		except ValueError:
			pass

		return line

def handleinstalline(parser,line):
	'''Parse and handle the given install command line.
	This is doing very simple-minded parsing for now.
	This is also very broken for the same reasons.'''
	tokens = quotedsplit(strip(line))
	debuglog('Input tokens: ' + `tokens`)

	if not tokens[0]:
		# It's a blank line--ignore it
		return 0

	if tokens[0][0] == '#':
		# It's a comment--ignore it
		return 0

	if tokens[0][0] == '(':
		# The following isn't quite right because we don't get the closing
		# parenthesis passed in
		printerr('Warning: subshells not properly supported')
		# Strip parenthesis and recursively call ourselves
		newline = join(quotedsplit(strip(line),openquote='(',closequote=')'))
		debuglog('Subshell line: '+newline)
		return handleinstalline(parser, newline)

	if find(line, '|') >= 0:
		printerr('Warning: command pipes not supported')
		return 1

	if find(line, '&') >= 0:
		printerr('Warning: background jobs are not supported')
		return 1

	if find(line, '<') >= 0:
		printerr('Warning: input redirection is not supported')
		return 1

	if find(tokens[0], '=') >= 0 or find(tokens[0], '$') >= 0:
		printerr('Warning: environment variables are not supported')
		return 1

	# Strip any leading path names on the command
	command = posixpath.basename(tokens[0])
	args = tokens[1:]
	debuglog("Handling '%s'" % command)

	if parser.parse(command,args):
		return 1
	return 0	# no errors handling command

def parseinstall(script,filelist):
	'''Parses the commands in script to find what files are copied

	Inputs: file containing install script
	        FileList object
	Returns: number of errors while parsing script
	'''
	errs = 0
	parser = ParseCommandLine(filelist)
	infile = ReadContLines(script)
	while 1:
		line = infile.readline()
		if not line:
			# End of file
			break
		errs = errs + handleinstalline(parser,line)

	return errs

def parseinstallwatch(infile,filelist):
	'''Parses the output of installwatch(1) to find what files are used

	Inputs: file containing installwatch output
	        FileList object
	Returns: number of errors while parsing script
	'''
	errs = 0
	parser = ParseInstallwatch(filelist)
	while 1:
		line = infile.readline()
		if not line:
			# End of file
			break

		tokens = split(strip(line),'\t')
		debuglog('Input tokens: ' + `tokens`)

		command = tokens[1]
		args = [tokens[0]] + tokens[2:]
		debuglog("Handling '%s'" % command)

		if parser.parse(command,args):
			errs = errs + 1

	return errs

def readfilelist(filesfile,filelist):
	'''Read the list of files and put into the filelist object.
	This function doesn't look at the current filesystem to get any info.
	'''
	errs = 0
	for fn in filesfile.readlines():
		name = strip(fn)
		if len(split(name)) > 1:
			errs = errs + 1
			printerr("Warning: doesn't look like a file name; try the -i option")
		if not posixpath.isabs(name):
			name = posixpath.join('/',name)
		f = filelist.getentry(name)
		if f.lookslikedir():
			f.setattrib(isdir=1)
	return errs

def findconfigurefile(dir):
	'Return non-zero if a configure file is found in the given directory'
	for fn in listdir(dir):
		if fn == 'configure':
			return 1
	return 0

def finddocfiles(dir):
	'Search for documentation files in the given directory'
	docfiles = []
	for fn in listdir(dir):
		if FileInfo(fn).lookslikedoc():
			docfiles.append(fn)
	docfiles.sort()
	return docfiles

def findlsmfiles(dir):
	'Search for .lsm files in the given directory'
	lsmfiles = filter(lambda fn: fn[-4:]=='.lsm', listdir(dir))
	lsmfiles.sort()
	return lsmfiles

def isbinary(data):
	'Return true if data is likely to be binary'
	return find(data, '\000') >= 0

def readparagraph(filename):
	'''Return a list containing the first paragraph (or so) of the given file.
	The paragraph is cleaned up by removing blank lines and spacer lines.'''
	MINLINES = 6	# Minimum number of lines in the paragraph
	MAXLINES = 12	# Maximum number of lines in the paragraph
	try:
		f = open(filename)
	except IOError:
		return None

	para = []
	while 1:
		line = f.readline()
		if not line or isbinary(line):
			break	# EOF or file is probably compressed
		line = strip(line)
		# If this is a spacer line like ------- or ********, treat it as blank
		if line and span(line,line[0]) == len(line):
			line = ''
		if not line:
			if len(para) > MINLINES:
				break	# End of paragraph; we have enough lines, so stop
			continue	# Skip over blank lines
		else:
			if len(para) > MAXLINES:
				break	# Middle of paragraph, but we have enough lines
		para.append(line)
	return para

# TODO: Look at the RFC and put all the legal characters here
url_re = regex.compile(
			  '\(\(http\|gopher\|ftp\|file\):'
			  '[a-z0-9~#%;/?:@=&$_.+!*''(),-]*\)'
			  , regex.casefold)

def findurl(strlist):
	'Search for what looks like a URL in a list of strings'
	for l in strlist:
		if url_re.search(l) >= 0:
			return url_re.group(1)
	return ''

def quotefilename(name):
	'Quote a file name if necessary to put it into the %files section'
	# Double quotes in a file name don't need additional quoting
	if regex.match('[a-zA-Z0-9\.+_=:/"-]*',name) != len(name):
		return '"'+name+'"'
	return name

def makearg(name):
	'Quote a file name if necessary to pass it to the shell'
	if regex.match('[a-zA-Z0-9\.+_=:/-]*',name) != len(name):
		return commands.mkarg(name)[1:]	# remove leading space
	return name

def install2spec(infile,outfile,options):
	'''Creates RPM spec file

	infile - file containing list of files
	outfile - write spec file to this file
	options - class containing configuration options
	'''
	class Spec:
		'Class to hold spec file fields'
		UNKNOWN_FIELD = 'unknown'	# string to use for unknown fields

		name = UNKNOWN_FIELD
		summary = UNKNOWN_FIELD
		group = UNKNOWN_FIELD
		source = UNKNOWN_FIELD
		version = UNKNOWN_FIELD
		release = UNKNOWN_FIELD
		copyright = UNKNOWN_FIELD
		distribution = UNKNOWN_FIELD
		changelog = UNKNOWN_FIELD
		url = UNKNOWN_FIELD
		prefix = UNKNOWN_FIELD
		buildarch = UNKNOWN_FIELD

		def __init__(self):
			self.description = []

	# Create object which holds the contents of the fields in the spec file
	spec = Spec()

	lsm = {}
	if options.uselsm:
		# Read an LSM file if it can be found
		lsmfiles = findlsmfiles('.')
		if lsmfiles:
			try:
				lsm = CanonicalLSM(open(lsmfiles[0],'r'))	# Use first name
			except:
				printerr("Can't use LSM file " + lsmfiles[0])
				lsm = {}

	# Determine RPM name and version number
	if options.rpmnamever:
		dirname = options.rpmnamever	# user specified name
	else:
		dirname = path.basename(getcwd())

	spec.version = classifyfile.extractver(dirname)
	if not spec.version:
		if lsm.has_key('version'):
			spec.version = joinstrings(lsm['version'])
		else:
			spec.version = spec.UNKNOWN_FIELD

	# LSM Title: is often a long form name and shouldn't be used as the RPM name
	spec.rpmname = dirname
	# if rpmname ends with the version string, strip it off
	if spec.rpmname[-len(spec.version):] == spec.version and \
		len(spec.rpmname) > len(spec.version):
		spec.rpmname = spec.rpmname[:-len(spec.version)]
		dirname = spec.rpmname + '%{version}' # make spec file easier to update
	if spec.rpmname[-1:] in ['-','_']:
		spec.rpmname = spec.rpmname[:-1]	# strip trailing - or _

	if lsm.has_key('title'):
		spec.summary = joinstrings(lsm['title'])
	else:
		spec.summary = spec.rpmname

	spec.release = '1'
	spec.distribution = DISTRIBUTION

	if options.copyright:
		spec.copyright = options.copyright
	else:
		if lsm.has_key('copying-policy'):
			spec.copyright = joinstrings(lsm['copying-policy'])
		else:
			spec.copyright = spec.UNKNOWN_FIELD

	filelist = FileListAnalyze()
	if options.filessource == options.FILES_INSTALL:
		# Look at the installation commands and get the files from it
		parseerrs = parseinstall(infile,filelist)

	elif options.filessource == options.FILES_LIST:
		# Read the list of files to use directly
		parseerrs = readfilelist(infile,filelist)

	elif options.filessource == options.FILES_INSTALLWATCH:
		# Read the output of installwatch to get the files to use
		parseerrs = parseinstallwatch(infile,filelist)

	spec.prefix = classifyfile.dirprefix(filelist.filenames())

	# Find additional documentation files
	docfiles = []
	foundconfigure = None
	if options.docfilepath:
		docfiles = finddocfiles(options.docfilepath)
		foundconfigure = findconfigurefile(options.docfilepath)

	if options.group:
		spec.group = options.group
	else:
		# Guess at the group by looking at the file names
		if len(filelist) > 0:
			guessed = filelist.guessgroup()
			if guessed:
				spec.group = guessed

	# Magic group name that means it's safe to make this a noarch package
	if spec.group[:13] == 'Documentation':
		spec.buildarch = 'noarch'

	spec.source = dirname
	if options.binarymode:
		spec.source = spec.source + '-bin'

	if options.zipmode:
		spec.source = spec.source + '.zip'
	else:
		spec.source = spec.source + '.tar.gz'

	if lsm.has_key('primary-site'):
		try:
			urls = fileurl(lsm['primary-site'])
			url = urls[0][0]
			if len(urls) > 1:
				# Choose a file that looks like an archive, if possible
				for u in urls:
					if find(u[0],'.tar') > 0 or \
					   find(u[0],'.tgz') > 0 or \
					   find(u[0],'.zip') > 0:
						url = u[0]
						break
			if find(posixpath.basename(urlparse(url)[2]),'.') < 0:
				# Looks like URL pointed to a directory, not an archive
				url = posixpath.join(url,spec.source)
			spec.source = url
		except BadSite:
			printerr("Badly-formatted Primary-Site: in LSM")

	spec.description = ''
	if lsm.has_key('description'):
		spec.description = lsm['description']
	elif options.docfilepath:
		# Use the first part of a README file as the description, if found.
		# Look through both the doc and regular files lists for README files.
		readmes = FileListAnalyze(docfiles[:]) + filelist
		for readme in readmes.onlyreadmefiles():
			# Read the first paragraph of the file
			para = readparagraph(readme.name)
			if para:
				spec.description = para
				break
	if not spec.description:
		# Still don't have a description
		spec.description = ['%s version %s' % (spec.rpmname, spec.version)]

	# Look for a URL embedded in the description
	spec.url = findurl(spec.description)
	if not spec.url:
		spec.url = spec.UNKNOWN_FIELD

	try:
		userinfo = getpwuid(getuid())
	except KeyError:
		userinfo = ['unknown','unknown',0,0,'','','']
	spec.changelog = '* %s %s <%s@%s>\n' % \
		(time.strftime('%a %b %d %Y',time.localtime(time.time())),
		userinfo[4],userinfo[0],uname()[1])

	spec.changelog = spec.changelog + \
			'- Initial spec file created by %s with rpm %g compatibility\n' \
			% (version_msg, options.rpmver)

	# Now that we have the basic spec information, try to fill in any gaps
	# intelligently
	if spec.copyright == spec.UNKNOWN_FIELD:
		copyright = lookforlicense(docfiles)
		if copyright:
			spec.copyright = copyright

	# Start writing RPM spec file
	writespec(outfile,options,spec,filelist,docfiles,dirname,foundconfigure)

	# Flush buffers to guarantee that the following printerr appears last
	outfile.flush()
	if parseerrs > 0:
		printerr('Warning: %d error(s) parsing install script' % parseerrs)
		return 2

	return 0

def filemacrosubst(fn):
	'Substitute a standard macro for a leading path component in a file name'

	# These are the useful macros available in Red Hat's rpm ver. 3
	# They are listed in most specific to least specific order in order to
	# make the most useful substitutions.
	# The directory names are actually regexes
	macros = [
		('^/usr/share/man\>', '%{_mandir}'),
		('^/usr/share/info\>', '%{_infodir}'),
		('^/usr/share\>', '%{_datadir}'),
		('^/usr/bin\>', '%{_bindir}'),
		('^/usr/sbin\>', '%{_sbindir}'),
		('^/usr/libexec\>', '%{_libexecdir}'),
		('^/usr/etc\>', '%{_sysconfdir}'),
		('^/usr/com\>', '%{_sharedstatedir}'),
		('^/usr/var\>', '%{_localstatedir}'),
		('^/usr/lib\>', '%{_libdir}'),
		('^/usr/include\>', '%{_includedir}'),
		('^/usr/info\>', '%{_infodir}'),
		('^/usr/man\>', '%{_mandir}'),
	]
	for path,macro in macros:
		fn = regsub.sub(path, macro, fn)
	return fn

def quotefilemacrosubst(fn):
	'Substitute standard macros in a file name and quote it if necessary'
	# Substituting a macro always makes it look like the file needs quoting
	# Check if it needs quoting before doing the substitution
	if fn != quotefilename(fn):
		# This should be made smart enough to quote the file name with the
		# macro in place. For now, don't do the macro replacement.
		return quotefilename(fn)
	return filemacrosubst(fn)

def writespec(outfile,options,spec,filelist,docfiles,dirname,foundconfigure):
	'Write a spec file using the supplied information'

	# Use BuildRoot when we can
	buildroot = options.binarymode or \
				options.filessource == options.FILES_LIST

	o = outfile
	o.write('# Initial spec file created by %s with rpm %g compatibility\n'
			% (version_msg, options.rpmver))
	o.write('Summary: %s\n' % spec.summary)
	if len(spec.summary) < 25:
		o.write('# The Summary: line should be expanded to about here -----^\n')
	if options.rpmver >= 2.5:
		for language in options.languages:
			o.write('#Summary(%s): (translated summary goes here)\n' % language)

	o.write('Name: %s\n' % spec.rpmname)		# required line

	if options.rpmver < 2.5:
		o.write('%%define version %s\n' % spec.version)
		o.write('Version: %{version}\n')	# required line
	else:
		o.write('Version: %s\n' % spec.version)	# required line

	o.write('Release: %s\n' % spec.release)	# required line
	if spec.group == spec.UNKNOWN_FIELD:
		o.write('#')
	o.write('Group: %s\n' % spec.group)		# required line
	if options.rpmver >= 2.5:
		for language in options.languages:
			o.write('#Group(%s): (translated group goes here)\n' % language)

	if spec.copyright == spec.UNKNOWN_FIELD:
		o.write('#')
	if options.rpmver >= 3.0:
		o.write('License: %s\n' % spec.copyright)		# required line
	else:
		o.write('Copyright: %s\n' % spec.copyright)		# required line

	# Replace a version number within the source location with a macro
	# This won't work if the version contains regex metacharacters, but oh, well
	sourceloc = regsub.sub('\<' + spec.version + '\>', '%{version}', spec.source)
	o.write('Source: %s\n' % sourceloc)
	if options.binarymode:
		o.write('#NoSource: 0\n')

	if not buildroot:
		o.write('#')	# BuildRoot in binary mode and without install script
	if options.rpmver >= 3.0:
		o.write('BuildRoot: %{_tmppath}/%{name}-root\n')
	else:
		if options.rpmver >= 2.5:
			o.write('BuildRoot: /tmp/%{name}-root\n')
		else:
			o.write('BuildRoot: /tmp/%s-root\n' % spec.rpmname)
	o.write('# Following are optional fields\n')

	if spec.url != spec.UNKNOWN_FIELD:
		o.write('URL: %s\n' % spec.url)
	else:
		o.write('#URL: http://www.example.net/%s/\n' % spec.rpmname)
	o.write('#Distribution: %s\n' % spec.distribution)
	o.write('#Patch: %s-%%{version}.patch\n' % spec.rpmname)
	o.write('#Prefix: %s\n' % spec.prefix)

	if spec.buildarch != 'noarch':
		o.write('#')
	if options.rpmver >= 3.0:
		o.write('BuildArch: noarch\n')
	else:
		o.write('BuildArchitectures: noarch\n')
	o.write('#Requires: \n')
	o.write('#Obsoletes: \n')
	if options.rpmver >= 3.0:
		o.write('#BuildRequires: \n')

	# End of header section
	o.write('\n')

	# Description is required
	# Should probably quote all % in description, but is this ever needed?
	o.write('%description\n')
	for l in spec.description:
		o.write('%s\n' % l)
	o.write('\n')

	if options.rpmver >= 2.5:
		for language in options.languages:
			o.write('#%%description -l %s\n' % language)
			o.write('#(translated description goes here)\n\n')

	# Extract the file name from what might be a URL
	sourcefile = posixpath.basename(urlparse(sourceloc)[2])

	# Default prep commands
	o.write('%prep\n')
	if options.zipmode:
		o.write('%%setup -T -c %s\n' % makearg(dirname))
		if options.rpmver >= 4.0:
			o.write('%__unzip')
		else:
			o.write('unzip')
		o.write(' -aL ${RPM_SOURCE_DIR}/%s\n' % makearg(sourcefile))
	else:
		if options.binarymode:
			o.write('%%setup -c %s\n' % makearg(dirname))
		else:
			if spec.version == spec.UNKNOWN_FIELD:
				o.write('%%setup -n %s\n' % makearg(dirname))
			else:
				o.write('%setup\n')
	o.write('#%patch\n')
	o.write('\n')

	# Default build and install commands
	if not options.binarymode:
		o.write('%build\n')
		if foundconfigure:
			if options.rpmver >= 3.0:
				o.write('%configure\n')
			else:
				o.write('env CFLAGS="$RPM_OPT_FLAGS" ./configure --prefix=/usr --sysconfdir=/etc\n')
		o.write('make\n')
		o.write('\n')

	o.write('%install\n')
	if options.binarymode:
		if options.rpmver >= 3.0:
			o.write('%__cp -a . "${RPM_BUILD_ROOT-/}"\n')
		else:
			o.write('cp -a . "${RPM_BUILD_ROOT-/}"\n')
	else:
		if options.filessource == options.FILES_LIST:
			createddirs = {'/':1}	# set of directories as we create each one
			for file in filelist:
				if options.rpmver >= 3.0:
					macrodirname = filemacrosubst(path.dirname(file.name))
					installname = '%__install'
					cpname = '%__cp'
				else:
					macrodirname = path.dirname(file.name)
					installname = 'install'
					cpname = 'cp'
				if not createddirs.has_key(path.dirname(file.name)):
					# Create the directory in the build root
					o.write('%s -d "${RPM_BUILD_ROOT}"%s\n' % 
							(installname, makearg(macrodirname)))
					createddirs[path.dirname(file.name)] = 1

				if not file.isdir:
					o.write('%s -a %s "${RPM_BUILD_ROOT}"%s\n' %
							(cpname,
							 makearg(path.basename(file.name)),
							 makearg(macrodirname)))
		else:
			if options.rpmver >= 3.0:
				o.write('%makeinstall\n')
			else:
				o.write('make install\n')
	o.write('\n')

	# Clean up after BuildRoot
	if not buildroot:
		o.write('#')
	o.write('%clean\n')
	if not buildroot:
		o.write('#')
	o.write('[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf "$RPM_BUILD_ROOT"\n')
	o.write('\n')

	# Postinstall/uninstall programs
	if filelist.isanylib():
		o.write('%post\n'
				'/sbin/ldconfig\n\n'
				'%postun\n'
				'/sbin/ldconfig\n\n')

	# List all directories and files which are part of package
	o.write('%files\n')
	if options.rpmver >= 2.5:
		o.write('%%defattr(-,%s,%s)\n' % (defaultowner,defaultgroup))

	# Files and directories
	for file in filelist:
		if (file.mode != None and file.mode != defaultmode \
		    and file.mode != defaultexemode) \
		   or (file.owner != None and file.owner != defaultowner \
		       and file.owner != defaultownernum) \
		   or (file.group != None and file.group != defaultgroup \
		       and file.group != defaultgroupnum):
			if file.mode == None:
				mode = '-'
			else:
				mode = '0%o' % file.mode
			owner = file.owner
			if not owner:
				owner = defaultowner
			group = file.group
			if not group:
				group = defaultgroup
			o.write('%%attr(%s,%s,%s) ' % (mode, owner, group))

		elif options.rpmver < 2.5 and buildroot:
			o.write('%%attr(%s,%s,%s) ' % ('-', defaultowner, defaultgroup))

		if file.isdir:
			o.write('%dir ')	# it's a directory
		elif file.mode == None or (file.mode & 0111) == 0:
			# These special files are not executable
			if file.lookslikeconfig():
				o.write('%config ')	# it's probably a config file
			elif options.rpmver >= 2.5 and \
				file.lookslikereadme() and posixpath.isabs(file.name):
				o.write('%readme ')
			elif options.rpmver >= 2.5 and \
				file.lookslikelicense() and posixpath.isabs(file.name):
				o.write('%license ')
			elif file.lookslikedoc():
				o.write('%doc ')	# it's probably a doc file
		if options.rpmver >= 3.0:
			o.write(quotefilemacrosubst(file.name))
		else:
			o.write(quotefilename(file.name))
		o.write('\n')

	# Extra documentation files
	for file in docfiles:
		if options.rpmver < 2.5 and buildroot:
			o.write('%%attr(%s,%s,%s) ' % ('-', defaultowner, defaultgroup))
		o.write('%doc ')
		if options.rpmver >= 3.0:
			o.write(quotefilemacrosubst(file))
		else:
			o.write(quotefilename(file))
		o.write('\n')
	o.write('\n')

	# Add change log section
	o.write('%%changelog\n%s' % spec.changelog)

def getdefaultlocale():
	'''Get the current locale
	This does basically the same thing as locale.getdefaultlocale in Python 2
	'''
	if not globals().has_key('locale'):
		# We're running an old Python without locale support
		return 'C'

	# Set the locale to the default to find out what default is
	current_locale = locale.setlocale(locale.LC_MESSAGES,None)
	try:
		default_locale = locale.setlocale(locale.LC_MESSAGES,'')
		# Set the locale back so autospec operation isn't affected
		locale.setlocale(locale.LC_MESSAGES,current_locale)
	except locale.Error:
		# Can't get the current locale; it's probably invalid
		return 'C'

	return default_locale

def get_language():
	"Get the current locale's language setting"
	language = lower(getdefaultlocale()[:2])

	# Only return a language if it's an interesting one
	if language not in ['c','posix']:
		language = language[:2]
		if language not in ['en']:
			return language

	return None

def usage():
	'Display program usage message and exit with error code'
	sys.stdout.write(version_msg+'\n')
	sys.stdout.write(usage_msg % path.basename(sys.argv[0]))
	sys.exit(1)

def show_license():
	'Display software license'
	sys.stdout.write(version_msg+'\n')
	sys.stdout.write(license)

def autospec():
	'Main command line interface to autospec'

	try:
		optlist, args = getopt.getopt(sys.argv[1:], '?bc:dg:hil:mn:r:wzL')
	except getopt.error:
		printerr('Unknown option')
		usage()		# display usage and exit

	if args:
		usage()		# display usage and exit

	options = Options()

	options.filessource = options.FILES_LIST

	# Put the current language into the default languages list
	language = get_language()
	if language:
		options.languages = [language]

	# Parse all options
	for opt,arg in optlist:
		if opt == '-h' or \
		   opt == '-?':
			usage()	# display usage and exit

		elif opt == '-L':
			show_license()
			sys.exit(0)

		elif opt == '-b':
			options.binarymode = 1

		elif opt == '-z':
			options.zipmode = 1

		elif opt == '-c':
			options.copyright = arg

		elif opt == '-g':
			options.group = arg

		elif opt == '-d':
			options.docfilepath = None

		elif opt == '-l':
			options.languages = filter(None, regsub.split(arg,'[,; \t]'))

			# Make sure each language code consists only of letters and dashes
			if not reduce(lambda a,x: a and span(x,letters+'-') == len(x),
						  options.languages, 1):
				printerr('Invalid language code (see RFC 1766)')
				sys.exit(1)

		elif opt == '-i':
			options.filessource = options.FILES_INSTALL

		elif opt == '-w':
			options.filessource = options.FILES_INSTALLWATCH

		elif opt == '-m':
			options.uselsm = 0

		elif opt == '-n':
			options.rpmnamever = arg

		elif opt == '-r':
			# Ignore everything after second decimal place, e.g. 2.4.12
			simplever = arg
			verlen = find(simplever,'.',find(simplever,'.')+1)
			if verlen >= 0:
				simplever = arg[:verlen]
			try:
				options.rpmver = round(atof(simplever),1)
			except ValueError:
				printerr('RPM version not numeric: ' + arg)
				sys.exit(1)

			if options.rpmver > 2.5:
				options.rpmver = 3.0
			elif options.rpmver < 2.5:
				options.rpmver = 2.4

		else:	# should never get here
			printerr('Unknown option ' + opt)
			usage()	# display usage and exit

	# Write a spec file from output of 'make -n install'
	sys.exit(install2spec(sys.stdin,sys.stdout,options))

if __name__=='__main__':
	autospec()
