A short time ago, I discovered the LinuxFromScratch project. After getting a system up and working (after much struggling), I realized that if I wanted to continue using LFS, some sort of package management would be quite nice. Of course I could have installed Pacman, apt-get, rpm, etc. like any sane person, perhaps. Instead, I was interested in creating my own simple 'package management' system that would keep track of files that belonged to a certain package, etc. I have attached two files:

 1. *package.py* – a class that describes information about a package such as its name, its version, what its dependencies are, etc
 2. *fakeroot.py* – this file is in charge of installing all of a package's files to the filesystem from a fakeroot, adding records of the installed files to a table in a database called Files, etc.

Here is *package.py*:
    
    import io_crate, os.path, sqlite3, core_regex, datetime, io_output

    class Package:
        crate_extension = '.crate'
        database_location = 'proto.db'
    
    	def __init__(self, name, verbosity = 0, fp = '/home/duncan/Documents/fakeroot', rp = '/home/duncan/Documents/install', ap = '/usr/src/archive/', cp = '/home/duncan/Documents/package/'):
    		# Setup database stuff
    		self.connection = sqlite3.connect(self.database_location)
    		self.connection.text_factory = str
    		self.db_cursor = self.connection.cursor()
    		
    		# Setup path and name
    		self.name = name
    		self.fakeroot_path = os.path.join(fp, self.name)
    		self.root = rp
    		self.archive_path = ap
    		self.crate_path = os.path.join(cp, self.name) + self.crate_extension
    		
    		# Setup description taken from .crate file
    		crate_contents = io_crate.read_crate(self.crate_path)
    		self.description = crate_contents[0][1]
    		self.homepage = crate_contents[1][1]
    		self.optional_deps = crate_contents[2][1]
    		self.recommended_deps = crate_contents[3][1]
    		self.required_deps = crate_contents[4][1]
    		
    		self.verbosity = verbosity
    
    	def add_to_db(self):
    		"""Adds self.name to the package database."""
    		if self.is_in_db():
    			return 0
    		else:
    			# no need to try..except this because is_in_db.
    			self.db_cursor.execute('INSERT INTO Packages VALUES(?, ?);', (self.name, datetime.datetime.today()))
    			io_output.vbprint(self.verbosity, '{} added to the databased'.format(self.name))
    			return 1
    		
    	def remove_from_db(self):
    		"""Removes self from the database of packages."""
    		if self.is_in_db():
    			self.db_cursor.execute('DELETE FROM Packages WHERE Package=?;', (self.name,))
    			io_output.vbprint(self.verbosity, '{} removed from database'.format(self.name))
    			return 1
    		return 0
    	
    	def is_in_db(self):
     		"""Checks if the name of self is contained in the packages database."""
     		try:
    			self.db_cursor.execute('SELECT * FROM Packages WHERE Package=?;', (self.name,))
    		except:
    			print 'Couldn\'t read the database at {}'.format(self.database_location)
    
    		if not self.db_cursor.fetchone():
    			return 0
    		return 1
    	
    	def __del__(self):
    		self.connection.commit()
    		self.connection.close()

and here is *fakeroot.py*:

    import os, md5_gen, shutil, io_output

    class Fakeroot():
    	def __init__(self, package):
    		self.dirs = [] # A list based on the files in the fakeroot.
    		self.files = [] # A list of all the directories to be created in package.root.
    		self.links = [] # A list of links from the fakeroot
    		self.package = package
    		
    		for root, dirs, files in os.walk(package.fakeroot_path):
    			for f in files:
    				new_dir = os.path.normpath(os.path.join(package.root, root[len(package.fakeroot_path) + 1:len(root)]))
    				src = os.path.join(root, f)
    				dest = os.path.join(new_dir, f) 
    				
    				if (os.path.islink(src)):
    					self.links.append([root, new_dir, f])
    				else:
    					self.files.append([src, dest])
    			for d in dirs:
    				self.dirs.append(os.path.join(package.root, root[len(package.fakeroot_path) + 1: len(root)], d))
    
    	def create_dirs(self):
    	# Go through self.dirs and check to see if a directory exists. If it does not, create it.
    		for d in self.dirs:
    			if not os.path.exists(d): # If the directory does not exist, run the equivalent of
    				os.makedirs(d) # mkdir -p on it.
    				io_output.vbprint(self.package.verbosity, 'DD {}'.format(d))
    			else:
    				io_output.vbprint(self.package.verbosity, 'UU {}'.format(d))
    				continue
    
    	def remove_dirs(self):
    		for d in reversed(self.dirs): # remove the directories that are the highest in the tree first.
    			if os.path.isdir(d): # If the directory exists
    				if not os.listdir(d): # and is empty...
    					os.rmdir(d) # remove it.
    					io_output.vbprint(self.package.verbosity, '-- {}'.format(d))
    				else: # If it is not empty.
    					io_output.vbprint(self.package.verbosity, 'UU {}'.format(d))
    			else: # If it does not exist.
    				io_output.vbprint(self.package.verbosity, '?? {}'.format(d))
    
    	def copy_files(self):
    		for f in self.files:
    			if os.path.exists(f[1]): # If the file exists, show that it is being used.
    				print 'Overwrite {}???'.format(f[1])
    				# TODO
    				# TODO: "Code" for overwiting stuff goes here.
    				# TODO: If yes, copy the file and add it to the DB.
    				# TODO: Perhaps an option to overwrite all files could be useful?
    				# TODO
    				continue
    			else: # If it does not exist,
    				try: # try...
    					shutil.copy2(f[0], f[1]) # copying it!
    					self.add_to_db(f)
    					io_output.vbprint(self.package.verbosity, '++ {}'.format(f[1]))
    				except:
    					io_output.vbprint(self.package.verbosity, 'Failed to copy a file...rolling back changes.')
    					#self.remove_files()
    					break
    
    	def remove_files(self):
    		for f in self.files:
    			if os.path.exists(f[1]):
    				os.remove(f[1])
    				self.remove_from_db(f)
    				io_output.vbprint(self.package.verbosity, '-- {}'.format(f[1]))
    			else:
    				io_output.vbprint(self.package.verbosity, '?? {}'.format(f[1]))
    
    	def create_links(self):
    		for l in self.links:
    			try:
    				if not os.path.exists(os.path.join(l[1], l[2])):
    					linkto = os.path.join(l[1], os.readlink(os.path.join(l[0], l[2])))
    					os.symlink(linkto, os.path.join(l[1], l[2]))
    					io_output.vbprint(self.package.verbosity, 'LL {}'.format(l[2]))
    				else:
    					print 'Overwrite existing link {}??'.format(l[2])
    					# TODO: See above todo for more info. ^^^^
    			except:
     				print 'Couldn\'t find the specified fakeroot!'
     				break
    				
    	def remove_links(self):
    		for l in self.links:
    			try:
    				os.remove(os.path.join(l[1], l[2]))
    				io_output.vbprint(self.package.verbosity, '-- {}.'.format(l))
    			except:
    				raise OSError('\nFailed to remove the link `{}`'.format(os.path.join(l[1], l[2])))
    
    	def add_to_db(self, f):
    		"""Returns 0 if the db can't be read, 1 if the file is added."""	
    		try:
    			self.package.db_cursor.execute('INSERT INTO Files VALUES(?, ?, ?, ?);', (f[1], md5_gen.generate(f[1]), 
    													os.path.getsize(f[1]),
    													self.package.name))
    			return 1
    		except:
    			return 0
    
    	def remove_from_db(self, f):
    		self.package.db_cursor.execute('DELETE FROM Files WHERE Path=?;', (f[1],))

In regard to these two files, I feel that my design is somewhat crappy. In particular, the design of the Package and Fakeroot objects seems `bad`. Additionally, the many similar methods in *fakeroot.py* for copying files / removing files seem somewhat redundant. What do you think?

Here is *io_output.py*:

    def vbprint(verb_l, message):
	    """Take a level of verboseness, `verb_l`, and a message to be printed. If verb_l > 0, the message is printed."""
	    if verb_l != 0:
		    print message


and here is *md5_gen.py* (the idea for this code was found by searching Google for something like `md5 python`:

    import hashlib

    def generate(filename):
	    """Return the md5 hash of filename.

	    Keyword arguments:
	    filename -- The path and name of the file to calculate the SHA1 hash of
	    """
	    # Returns the md5 hash of the given file.
	    md5 = hashlib.md5()

	    with open(filename, 'rb') as file:
		    while True:
			    block = file.read(2**10)
			    if not block:
				    break
			    md5.update(block)
		    return md5.hexdigest()

Here's *io_crate.py* -- used to get information from files about packages:
    
    from re import split

    def read_crate(package):
	    """Opens a crate file, reads the variables, and returns them as a sorted list of tuples.

	    Keyword arguments:
	    package -- A Package object, as defined in package.py
	    """
	    # Returns 0 if the crate file cannot be read, 1 on success.
	    try:
		    crate_text = open(package).read()
	    except IOError:
		    print 'Failed to read the crate file {}.'.format(package.crate_path)
		    return 0

	    processed = split("=.*?\"|\".*?\n", crate_text)

	    lastline = ""
	    final = []

	    for line in processed:
		    if lastline.isupper():
			    if '_DEP' in lastline:
				    final.append((lastline, split(' ', line)))
			    else:
				    final.append((lastline, line))
		    lastline = line
	    return sorted(final, key=lambda pos: pos[0])

and finally...`whew`...here is a sample `.crate` file:

    DESCRIPTION="The most foo of all bars."
    HOMEPAGE="http://www.foo.it/"   
    REQUIRED_DEP="foobar-11-2"
    RECOMMENDED_DEP="foobus-1.7"    
    OPTIONAL_DEP="foon-7.6a"