updated py2deb
[theonering] / support / py2deb.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 ##
4 ##    Copyright (C) 2009 manatlan manatlan[at]gmail(dot)com
5 ##
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published
8 ## by the Free Software Foundation; version 2 only.
9 ##
10 ## This program is distributed in the hope that it will be useful,
11 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 ## GNU General Public License for more details.
14 ##
15 """
16 Known limitations :
17 - don't sign package (-us -uc)
18 - no distinctions between author and maintainer(packager)
19
20 depends on :
21 - dpkg-dev (dpkg-buildpackage)
22 - alien
23 - python
24 - fakeroot
25
26 changelog
27  - ??? ?/??/20?? (By epage)
28     - PEP8
29     - added recommends
30     - fixed bug where it couldn't handle the contents of the pre/post scripts being specified
31     - Added customization based on the targeted policy for sections (Maemo support)
32     - Added maemo specific tarball, dsc, changes file generation support (including icon support)
33     - Added armel architecture
34     - Reduced the size of params being passed around by reducing the calls to locals()
35     - Added respository, distribution, priority
36     - Made setting control file a bit more flexible
37  - 0.5 05/09/2009
38     - pre/post install/remove scripts enabled
39     - deb package install py2deb in dist-packages for py2.6
40  - 0.4 14/10/2008
41     - use os.environ USERNAME or USER (debian way)
42     - install on py 2.(4,5,6) (*FIX* do better here)
43
44 """
45
46 import os
47 import hashlib
48 import sys
49 import shutil
50 import time
51 import string
52 import StringIO
53 import stat
54 import commands
55 import base64
56 import tarfile
57 from glob import glob
58 from datetime import datetime
59 import socket # gethostname()
60 from subprocess import Popen, PIPE
61
62 #~ __version__ = "0.4"
63 __version__ = "0.5"
64 __author__ = "manatlan"
65 __mail__ = "manatlan@gmail.com"
66
67
68 PERMS_URW_GRW_OR = stat.S_IRUSR | stat.S_IWUSR | \
69                    stat.S_IRGRP | stat.S_IWGRP | \
70                    stat.S_IROTH
71
72 UID_ROOT = 0
73 GID_ROOT = 0
74
75
76 def run(cmds):
77     p = Popen(cmds, shell=False, stdout=PIPE, stderr=PIPE)
78     time.sleep(0.01)    # to avoid "IOError: [Errno 4] Interrupted system call"
79     out = string.join(p.stdout.readlines()).strip()
80     outerr = string.join(p.stderr.readlines()).strip()
81     return out
82
83
84 def deb2rpm(file):
85     txt=run(['alien', '-r', file])
86     return txt.split(" generated")[0]
87
88
89 def py2src(TEMP, name):
90     l=glob("%(TEMP)s/%(name)s*.tar.gz" % locals())
91     if len(l) != 1:
92         raise Py2debException("don't find source package tar.gz")
93
94     tar = os.path.basename(l[0])
95     shutil.move(l[0], tar)
96
97     return tar
98
99
100 def md5sum(filename):
101     f = open(filename, "r")
102     try:
103         return hashlib.md5(f.read()).hexdigest()
104     finally:
105         f.close()
106
107
108 class Py2changes(object):
109
110     def __init__(self, ChangedBy, description, changes, files, category, repository, **kwargs):
111       self.options = kwargs # TODO: Is order important?
112       self.description = description
113       self.changes=changes
114       self.files=files
115       self.category=category
116       self.repository=repository
117       self.ChangedBy=ChangedBy
118
119     def getContent(self):
120         content = ["%s: %s" % (k, v)
121                    for k,v in self.options.iteritems()]
122
123         if self.description:
124             description=self.description.replace("\n","\n ")
125             content.append('Description: ')
126             content.append(' %s' % description)
127         if self.changes:
128             changes=self.changes.replace("\n","\n ")
129             content.append('Changes: ')
130             content.append(' %s' % changes)
131         if self.ChangedBy:
132             content.append("Changed-By: %s" % self.ChangedBy)
133
134         content.append('Files:')
135
136         for onefile in self.files:
137             md5 = md5sum(onefile)
138             size = os.stat(onefile).st_size.__str__()
139             content.append(' ' + md5 + ' ' + size + ' ' + self.category +' '+self.repository+' '+os.path.basename(onefile))
140
141         return "\n".join(content) + "\n\n"
142
143
144 def py2changes(params):
145     changescontent = Py2changes(
146         "%(author)s <%(mail)s>" % params,
147         "%(description)s" % params,
148         "%(changelog)s" % params,
149         (
150             "%(TEMP)s/%(name)s_%(version)s.tar.gz" % params,
151             "%(TEMP)s/%(name)s_%(version)s.dsc" % params,
152         ),
153         "%(section)s" % params,
154         "%(repository)s" % params,
155         Format='1.7',
156         Date=time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()),
157         Source="%(name)s" % params,
158         Architecture="%(arch)s" % params,
159         Version="%(version)s" % params,
160         Distribution="%(distribution)s" % params,
161         Urgency="%(urgency)s" % params,
162         Maintainer="%(author)s <%(mail)s>" % params
163     )
164     f = open("%(TEMP)s/%(name)s_%(version)s.changes" % params,"wb")
165     f.write(changescontent.getContent())
166     f.close()
167
168     fileHandle = open('/tmp/py2deb2.tmp', 'w')
169     fileHandle.write('#!/bin/sh\n')
170     fileHandle.write("cd " +os.getcwd()+ "\n")
171     fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.changes\n" % params)
172     fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.changes.asc %(TEMP)s/%(name)s_%(version)s.changes\n" % params)
173     fileHandle.write('\nexit')
174     fileHandle.close()
175     commands.getoutput("chmod 777 /tmp/py2deb2.tmp")
176     commands.getoutput("/tmp/py2deb2.tmp")
177
178     ret = []
179
180     l=glob("%(TEMP)s/%(name)s*.tar.gz" % params)
181     if len(l)!=1:
182         raise Py2debException("don't find source package tar.gz")
183     tar = os.path.basename(l[0])
184     shutil.move(l[0],tar)
185     ret.append(tar)
186
187     l=glob("%(TEMP)s/%(name)s*.dsc" % params)
188     if len(l)!=1:
189         raise Py2debException("don't find source package dsc")
190     tar = os.path.basename(l[0])
191     shutil.move(l[0],tar)
192     ret.append(tar)
193
194     l=glob("%(TEMP)s/%(name)s*.changes" % params)
195     if len(l)!=1:
196         raise Py2debException("don't find source package changes")
197     tar = os.path.basename(l[0])
198     shutil.move(l[0],tar)
199     ret.append(tar)
200
201     return ret
202
203
204 class Py2dsc(object):
205
206     def __init__(self, StandardsVersion, BuildDepends, files, **kwargs):
207       self.options = kwargs # TODO: Is order important?
208       self.StandardsVersion = StandardsVersion
209       self.BuildDepends=BuildDepends
210       self.files=files
211
212     @property
213     def content(self):
214         content = ["%s: %s" % (k, v)
215                    for k,v in self.options.iteritems()]
216
217         if self.BuildDepends:
218             content.append("Build-Depends: %s" % self.BuildDepends)
219         if self.StandardsVersion:
220             content.append("Standards-Version: %s" % self.StandardsVersion)
221
222         content.append('Files:')
223
224         for onefile in self.files:
225             print onefile
226             md5 = md5sum(onefile)
227             size = os.stat(onefile).st_size.__str__()
228             content.append(' '+md5 + ' ' + size +' '+os.path.basename(onefile))
229
230         return "\n".join(content)+"\n\n"
231
232
233 def py2dsc(TEMP, name, version, depends, author, mail, arch):
234     dsccontent = Py2dsc(
235         "%(version)s" % locals(),
236         "%(depends)s" % locals(),
237         ("%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals(),),
238         Format='1.0',
239         Source="%(name)s" % locals(),
240         Version="%(version)s" % locals(),
241         Maintainer="%(author)s <%(mail)s>" % locals(),
242         Architecture="%(arch)s" % locals(),
243     )
244
245     filename = "%(TEMP)s/%(name)s_%(version)s.dsc" % locals()
246
247     f = open(filename, "wb")
248     try:
249         f.write(dsccontent.content)
250     finally:
251         f.close()
252
253     fileHandle = open('/tmp/py2deb.tmp', 'w')
254     try:
255         fileHandle.write('#!/bin/sh\n')
256         fileHandle.write("cd " + os.getcwd() + "\n")
257         fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.dsc\n" % locals())
258         fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.dsc.asc %(filename)s\n" % locals())
259         fileHandle.write('\nexit')
260         fileHandle.close()
261     finally:
262         f.close()
263
264     commands.getoutput("chmod 777 /tmp/py2deb.tmp")
265     commands.getoutput("/tmp/py2deb.tmp")
266
267     return filename
268
269
270 class Py2tar(object):
271
272     def __init__(self, dataDirectoryPath):
273         self._dataDirectoryPath = dataDirectoryPath
274
275     def packed(self):
276         return self._getSourcesFiles()
277
278     def _getSourcesFiles(self):
279         directoryPath = self._dataDirectoryPath
280
281         outputFileObj = StringIO.StringIO() # TODO: Do more transparently?
282
283         tarOutput = tarfile.TarFile.open('sources',
284                                  mode = "w:gz",
285                                  fileobj = outputFileObj)
286
287         # Note: We can't use this because we need to fiddle permissions:
288         #       tarOutput.add(directoryPath, arcname = "")
289
290         for root, dirs, files in os.walk(directoryPath):
291             archiveRoot = root[len(directoryPath):]
292
293             tarinfo = tarOutput.gettarinfo(root, archiveRoot)
294             # TODO: Make configurable?
295             tarinfo.uid = UID_ROOT
296             tarinfo.gid = GID_ROOT
297             tarinfo.uname = ""
298             tarinfo.gname = ""
299             tarOutput.addfile(tarinfo)
300
301             for f in  files:
302                 tarinfo = tarOutput.gettarinfo(os.path.join(root, f),
303                                                os.path.join(archiveRoot, f))
304                 tarinfo.uid = UID_ROOT
305                 tarinfo.gid = GID_ROOT
306                 tarinfo.uname = ""
307                 tarinfo.gname = ""
308                 tarOutput.addfile(tarinfo, file(os.path.join(root, f)))
309
310         tarOutput.close()
311
312         data_tar_gz = outputFileObj.getvalue()
313
314         return data_tar_gz
315
316
317 def py2tar(DEST, TEMP, name, version):
318     tarcontent = Py2tar("%(DEST)s" % locals())
319     filename = "%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals()
320     f = open(filename, "wb")
321     try:
322         f.write(tarcontent.packed())
323     finally:
324         f.close()
325     return filename
326
327
328 class Py2debException(Exception):
329     pass
330
331
332 SECTIONS_BY_POLICY = {
333     # http://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections
334     "debian": "admin, base, comm, contrib, devel, doc, editors, electronics, embedded, games, gnome, graphics, hamradio, interpreters, kde, libs, libdevel, mail, math, misc, net, news, non-free, oldlibs, otherosfs, perl, python, science, shells, sound, tex, text, utils, web, x11",
335     # http://maemo.org/forrest-images/pdf/maemo-policy.pdf
336     "chinook": "accessories, communication, games, multimedia, office, other, programming, support, themes, tools",
337     # http://wiki.maemo.org/Task:Package_categories
338     "diablo": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities",
339     # http://wiki.maemo.org/Task:Fremantle_application_categories
340     "mer": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities",
341     # http://wiki.maemo.org/Task:Fremantle_application_categories
342     "fremantle": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities",
343 }
344
345
346 LICENSE_AGREEMENT = {
347         "gpl": """
348     This package is free software; you can redistribute it and/or modify
349     it under the terms of the GNU General Public License as published by
350     the Free Software Foundation; either version 2 of the License, or
351     (at your option) any later version.
352
353     This package is distributed in the hope that it will be useful,
354     but WITHOUT ANY WARRANTY; without even the implied warranty of
355     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
356     GNU General Public License for more details.
357
358     You should have received a copy of the GNU General Public License
359     along with this package; if not, write to the Free Software
360     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
361
362 On Debian systems, the complete text of the GNU General
363 Public License can be found in `/usr/share/common-licenses/GPL'.
364 """,
365         "lgpl":"""
366     This package is free software; you can redistribute it and/or
367     modify it under the terms of the GNU Lesser General Public
368     License as published by the Free Software Foundation; either
369     version 2 of the License, or (at your option) any later version.
370
371     This package is distributed in the hope that it will be useful,
372     but WITHOUT ANY WARRANTY; without even the implied warranty of
373     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
374     Lesser General Public License for more details.
375
376     You should have received a copy of the GNU Lesser General Public
377     License along with this package; if not, write to the Free Software
378     Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
379
380 On Debian systems, the complete text of the GNU Lesser General
381 Public License can be found in `/usr/share/common-licenses/LGPL'.
382 """,
383         "bsd": """
384     Redistribution and use in source and binary forms, with or without
385     modification, are permitted under the terms of the BSD License.
386
387     THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
388     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
389     IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
390     ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
391     FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
392     DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
393     OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
394     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
395     LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
396     OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
397     SUCH DAMAGE.
398
399 On Debian systems, the complete text of the BSD License can be
400 found in `/usr/share/common-licenses/BSD'.
401 """,
402         "artistic": """
403     This program is free software; you can redistribute it and/or modify it
404     under the terms of the "Artistic License" which comes with Debian.
405
406     THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
407     WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES
408     OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
409
410 On Debian systems, the complete text of the Artistic License
411 can be found in `/usr/share/common-licenses/Artistic'.
412 """
413 }
414
415
416 class Py2deb(object):
417     """
418     heavily based on technic described here :
419     http://wiki.showmedo.com/index.php?title=LinuxJensMakingDeb
420     """
421     ## STATICS
422     clear = False  # clear build folder after py2debianization
423
424     SECTIONS = SECTIONS_BY_POLICY["debian"]
425
426     #http://www.debian.org/doc/debian-policy/footnotes.html#f69
427     ARCHS = "all i386 ia64 alpha amd64 armeb arm hppa m32r m68k mips mipsel powerpc ppc64 s390 s390x sh3 sh3eb sh4 sh4eb sparc darwin-i386 darwin-ia64 darwin-alpha darwin-amd64 darwin-armeb darwin-arm darwin-hppa darwin-m32r darwin-m68k darwin-mips darwin-mipsel darwin-powerpc darwin-ppc64 darwin-s390 darwin-s390x darwin-sh3 darwin-sh3eb darwin-sh4 darwin-sh4eb darwin-sparc freebsd-i386 freebsd-ia64 freebsd-alpha freebsd-amd64 freebsd-armeb freebsd-arm freebsd-hppa freebsd-m32r freebsd-m68k freebsd-mips freebsd-mipsel freebsd-powerpc freebsd-ppc64 freebsd-s390 freebsd-s390x freebsd-sh3 freebsd-sh3eb freebsd-sh4 freebsd-sh4eb freebsd-sparc kfreebsd-i386 kfreebsd-ia64 kfreebsd-alpha kfreebsd-amd64 kfreebsd-armeb kfreebsd-arm kfreebsd-hppa kfreebsd-m32r kfreebsd-m68k kfreebsd-mips kfreebsd-mipsel kfreebsd-powerpc kfreebsd-ppc64 kfreebsd-s390 kfreebsd-s390x kfreebsd-sh3 kfreebsd-sh3eb kfreebsd-sh4 kfreebsd-sh4eb kfreebsd-sparc knetbsd-i386 knetbsd-ia64 knetbsd-alpha knetbsd-amd64 knetbsd-armeb knetbsd-arm knetbsd-hppa knetbsd-m32r knetbsd-m68k knetbsd-mips knetbsd-mipsel knetbsd-powerpc knetbsd-ppc64 knetbsd-s390 knetbsd-s390x knetbsd-sh3 knetbsd-sh3eb knetbsd-sh4 knetbsd-sh4eb knetbsd-sparc netbsd-i386 netbsd-ia64 netbsd-alpha netbsd-amd64 netbsd-armeb netbsd-arm netbsd-hppa netbsd-m32r netbsd-m68k netbsd-mips netbsd-mipsel netbsd-powerpc netbsd-ppc64 netbsd-s390 netbsd-s390x netbsd-sh3 netbsd-sh3eb netbsd-sh4 netbsd-sh4eb netbsd-sparc openbsd-i386 openbsd-ia64 openbsd-alpha openbsd-amd64 openbsd-armeb openbsd-arm openbsd-hppa openbsd-m32r openbsd-m68k openbsd-mips openbsd-mipsel openbsd-powerpc openbsd-ppc64 openbsd-s390 openbsd-s390x openbsd-sh3 openbsd-sh3eb openbsd-sh4 openbsd-sh4eb openbsd-sparc hurd-i386 hurd-ia64 hurd-alpha hurd-amd64 hurd-armeb hurd-arm hurd-hppa hurd-m32r hurd-m68k hurd-mips hurd-mipsel hurd-powerpc hurd-ppc64 hurd-s390 hurd-s390x hurd-sh3 hurd-sh3eb hurd-sh4 hurd-sh4eb hurd-sparc armel".split(" ")
428
429     # license terms taken from dh_make
430     LICENSES = list(LICENSE_AGREEMENT.iterkeys())
431
432     def __setitem__(self, path, files):
433
434         if not type(files)==list:
435             raise Py2debException("value of key path '%s' is not a list"%path)
436         if not files:
437             raise Py2debException("value of key path '%s' should'nt be empty"%path)
438         if not path.startswith("/"):
439             raise Py2debException("key path '%s' malformed (don't start with '/')"%path)
440         if path.endswith("/"):
441             raise Py2debException("key path '%s' malformed (shouldn't ends with '/')"%path)
442
443         nfiles=[]
444         for file in files:
445
446             if ".." in file:
447                 raise Py2debException("file '%s' contains '..', please avoid that!"%file)
448
449
450             if "|" in file:
451                 if file.count("|")!=1:
452                     raise Py2debException("file '%s' is incorrect (more than one pipe)"%file)
453
454                 file, nfile = file.split("|")
455             else:
456                 nfile=file  # same localisation
457
458             if os.path.isdir(file):
459                 raise Py2debException("file '%s' is a folder, and py2deb refuse folders !"%file)
460
461             if not os.path.isfile(file):
462                 raise Py2debException("file '%s' doesn't exist"%file)
463
464             if file.startswith("/"):    # if an absolute file is defined
465                 if file==nfile:         # and not renamed (pipe trick)
466                     nfile=os.path.basename(file)   # it's simply copied to 'path'
467
468             nfiles.append((file, nfile))
469
470         nfiles.sort(lambda a, b: cmp(a[1], b[1]))    #sort according new name (nfile)
471
472         self.__files[path]=nfiles
473
474     def __delitem__(self, k):
475         del self.__files[k]
476
477     def __init__(self,
478                     name,
479                     description="no description",
480                     license="gpl",
481                     depends="",
482                     section="utils",
483                     arch="all",
484
485                     url="",
486                     author = None,
487                     mail = None,
488
489                     preinstall = None,
490                     postinstall = None,
491                     preremove = None,
492                     postremove = None
493                 ):
494
495         if author is None:
496             author = ("USERNAME" in os.environ) and os.environ["USERNAME"] or None
497             if author is None:
498                 author = ("USER" in os.environ) and os.environ["USER"] or "unknown"
499
500         if mail is None:
501             mail = author+"@"+socket.gethostname()
502
503         self.name = name
504         self.description = description
505         self.upgradeDescription = ""
506         self.license = license
507         self.depends = depends
508         self.recommends = ""
509         self.section = section
510         self.arch = arch
511         self.url = url
512         self.author = author
513         self.mail = mail
514         self.icon = ""
515         self.distribution = ""
516         self.respository = ""
517         self.urgency = "low"
518
519         self.preinstall = preinstall
520         self.postinstall = postinstall
521         self.preremove = preremove
522         self.postremove = postremove
523
524         self.__files={}
525
526     def __repr__(self):
527         name = self.name
528         license = self.license
529         description = self.description
530         depends = self.depends
531         recommends = self.recommends
532         section = self.section
533         arch = self.arch
534         url = self.url
535         author = self.author
536         mail = self.mail
537
538         preinstall = self.preinstall
539         postinstall = self.postinstall
540         preremove = self.preremove
541         postremove = self.postremove
542
543         paths=self.__files.keys()
544         paths.sort()
545         files=[]
546         for path in paths:
547             for file, nfile in self.__files[path]:
548                 #~ rfile=os.path.normpath(os.path.join(path, nfile))
549                 rfile=os.path.join(path, nfile)
550                 if nfile==file:
551                     files.append(rfile)
552                 else:
553                     files.append(rfile + " (%s)"%file)
554
555         files.sort()
556         files = "\n".join(files)
557
558
559         lscripts = [    preinstall and "preinst",
560                         postinstall and "postinst",
561                         preremove and "prerm",
562                         postremove and "postrm",
563                     ]
564         scripts = lscripts and ", ".join([i for i in lscripts if i]) or "None"
565         return """
566 ----------------------------------------------------------------------
567 NAME        : %(name)s
568 ----------------------------------------------------------------------
569 LICENSE     : %(license)s
570 URL         : %(url)s
571 AUTHOR      : %(author)s
572 MAIL        : %(mail)s
573 ----------------------------------------------------------------------
574 DEPENDS     : %(depends)s
575 RECOMMENDS  : %(recommends)s
576 ARCH        : %(arch)s
577 SECTION     : %(section)s
578 ----------------------------------------------------------------------
579 DESCRIPTION :
580 %(description)s
581 ----------------------------------------------------------------------
582 SCRIPTS : %(scripts)s
583 ----------------------------------------------------------------------
584 FILES :
585 %(files)s
586 """ % locals()
587
588     def generate(self, version, changelog="", rpm=False, src=False, build=True, tar=False, changes=False, dsc=False):
589         """ generate a deb of version 'version', with or without 'changelog', with or without a rpm
590             (in the current folder)
591             return a list of generated files
592         """
593         if not sum([len(i) for i in self.__files.values()])>0:
594             raise Py2debException("no files are defined")
595
596         if not changelog:
597             changelog="* no changelog"
598
599         name = self.name
600         description = self.description
601         license = self.license
602         depends = self.depends
603         recommends = self.recommends
604         section = self.section
605         arch = self.arch
606         url = self.url
607         distribution = self.distribution
608         repository = self.repository
609         urgency = self.urgency
610         author = self.author
611         mail = self.mail
612         files = self.__files
613         preinstall = self.preinstall
614         postinstall = self.postinstall
615         preremove = self.preremove
616         postremove = self.postremove
617
618         if section not in Py2deb.SECTIONS:
619             raise Py2debException("section '%s' is unknown (%s)" % (section, str(Py2deb.SECTIONS)))
620
621         if arch not in Py2deb.ARCHS:
622             raise Py2debException("arch '%s' is unknown (%s)"% (arch, str(Py2deb.ARCHS)))
623
624         if license not in Py2deb.LICENSES:
625             raise Py2debException("License '%s' is unknown (%s)" % (license, str(Py2deb.LICENSES)))
626
627         # create dates (buildDate, buildDateYear)
628         d=datetime.now()
629         buildDate=d.strftime("%a, %d %b %Y %H:%M:%S +0000")
630         buildDateYear=str(d.year)
631
632         #clean description (add a space before each next lines)
633         description=description.replace("\r", "").strip()
634         description = "\n ".join(description.split("\n"))
635
636         #clean changelog (add 2 spaces before each next lines)
637         changelog=changelog.replace("\r", "").strip()
638         changelog = "\n  ".join(changelog.split("\n"))
639
640         TEMP = ".py2deb_build_folder"
641         DEST = os.path.join(TEMP, name)
642         DEBIAN = os.path.join(DEST, "debian")
643
644         packageContents = locals()
645
646         # let's start the process
647         try:
648             shutil.rmtree(TEMP)
649         except:
650             pass
651
652         os.makedirs(DEBIAN)
653         try:
654             rules=[]
655             dirs=[]
656             for path in files:
657                 for ofile, nfile in files[path]:
658                     if os.path.isfile(ofile):
659                         # it's a file
660
661                         if ofile.startswith("/"): # if absolute path
662                             # we need to change dest
663                             dest=os.path.join(DEST, nfile)
664                         else:
665                             dest=os.path.join(DEST, ofile)
666
667                         # copy file to be packaged
668                         destDir = os.path.dirname(dest)
669                         if not os.path.isdir(destDir):
670                             os.makedirs(destDir)
671
672                         shutil.copy2(ofile, dest)
673
674                         ndir = os.path.join(path, os.path.dirname(nfile))
675                         nname = os.path.basename(nfile)
676
677                         # make a line RULES to be sure the destination folder is created
678                         # and one for copying the file
679                         fpath = "/".join(["$(CURDIR)", "debian", name+ndir])
680                         rules.append('mkdir -p "%s"' % fpath)
681                         rules.append('cp -a "%s" "%s"' % (ofile, os.path.join(fpath, nname)))
682
683                         # append a dir
684                         dirs.append(ndir)
685
686                     else:
687                         raise Py2debException("unknown file '' "%ofile) # shouldn't be raised (because controlled before)
688
689             # make rules right
690             rules= "\n\t".join(rules) + "\n"
691             packageContents["rules"] = rules
692
693             # make dirs right
694             dirs= [i[1:] for i in set(dirs)]
695             dirs.sort()
696
697             #==========================================================================
698             # CREATE debian/dirs
699             #==========================================================================
700             open(os.path.join(DEBIAN, "dirs"), "w").write("\n".join(dirs))
701
702             #==========================================================================
703             # CREATE debian/changelog
704             #==========================================================================
705             clog="""%(name)s (%(version)s) stable; urgency=low
706
707   %(changelog)s
708
709  -- %(author)s <%(mail)s>  %(buildDate)s
710 """ % packageContents
711
712             open(os.path.join(DEBIAN, "changelog"), "w").write(clog)
713
714             #==========================================================================
715             #Create pre/post install/remove
716             #==========================================================================
717             def mkscript(name, dest):
718                 if name and name.strip()!="":
719                     if os.path.isfile(name):    # it's a file
720                         content = file(name).read()
721                     else:   # it's a script
722                         content = name
723                     open(os.path.join(DEBIAN, dest), "w").write(content)
724
725             mkscript(preinstall, "preinst")
726             mkscript(postinstall, "postinst")
727             mkscript(preremove, "prerm")
728             mkscript(postremove, "postrm")
729
730
731             #==========================================================================
732             # CREATE debian/compat
733             #==========================================================================
734             open(os.path.join(DEBIAN, "compat"), "w").write("5\n")
735
736             #==========================================================================
737             # CREATE debian/control
738             #==========================================================================
739             generalParagraphFields = [
740                 "Source: %(name)s",
741                 "Maintainer: %(author)s <%(mail)s>",
742                 "Section: %(section)s",
743                 "Priority: extra",
744                 "Build-Depends: debhelper (>= 5)",
745                 "Standards-Version: 3.7.2",
746             ]
747
748             specificParagraphFields = [
749                 "Package: %(name)s",
750                 "Architecture: %(arch)s",
751                 "Depends: %(depends)s",
752                 "Recommends: %(recommends)s",
753                 "Description: %(description)s",
754             ]
755
756             if self.upgradeDescription:
757                 upgradeDescription = "XB-Maemo-Upgrade-Description: %s" % self.upgradeDescription.strip()
758                 specificParagraphFields.append("\n  ".join(upgradeDescription.split("\n")))
759
760             if self.icon:
761                 f = open(self.icon, "rb")
762                 try:
763                     rawIcon = f.read()
764                 finally:
765                     f.close()
766                 uueIcon = base64.b64encode(rawIcon)
767                 uueIconLines = []
768                 for i, c in enumerate(uueIcon):
769                     if i % 60 == 0:
770                         uueIconLines.append("")
771                     uueIconLines[-1] += c
772                 uueIconLines[0:0] = ("XB-Maemo-Icon-26:", )
773                 specificParagraphFields.append("\n  ".join(uueIconLines))
774
775             generalParagraph = "\n".join(generalParagraphFields)
776             specificParagraph = "\n".join(specificParagraphFields)
777             controlContent = "\n\n".join((generalParagraph, specificParagraph)) % packageContents
778             open(os.path.join(DEBIAN, "control"), "w").write(controlContent)
779
780             #==========================================================================
781             # CREATE debian/copyright
782             #==========================================================================
783             packageContents["txtLicense"] = LICENSE_AGREEMENT[license]
784             packageContents["pv"] =__version__
785             txt="""This package was py2debianized(%(pv)s) by %(author)s <%(mail)s> on
786 %(buildDate)s.
787
788 It was downloaded from %(url)s
789
790 Upstream Author: %(author)s <%(mail)s>
791
792 Copyright: %(buildDateYear)s by %(author)s
793
794 License:
795
796 %(txtLicense)s
797
798 The Debian packaging is (C) %(buildDateYear)s, %(author)s <%(mail)s> and
799 is licensed under the GPL, see above.
800
801
802 # Please also look if there are files or directories which have a
803 # different copyright/license attached and list them here.
804 """ % packageContents
805             open(os.path.join(DEBIAN, "copyright"), "w").write(txt)
806
807             #==========================================================================
808             # CREATE debian/rules
809             #==========================================================================
810             txt="""#!/usr/bin/make -f
811 # -*- makefile -*-
812 # Sample debian/rules that uses debhelper.
813 # This file was originally written by Joey Hess and Craig Small.
814 # As a special exception, when this file is copied by dh-make into a
815 # dh-make output file, you may use that output file without restriction.
816 # This special exception was added by Craig Small in version 0.37 of dh-make.
817
818 # Uncomment this to turn on verbose mode.
819 #export DH_VERBOSE=1
820
821
822
823
824 CFLAGS = -Wall -g
825
826 ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
827         CFLAGS += -O0
828 else
829         CFLAGS += -O2
830 endif
831
832 configure: configure-stamp
833 configure-stamp:
834         dh_testdir
835         # Add here commands to configure the package.
836
837         touch configure-stamp
838
839
840 build: build-stamp
841
842 build-stamp: configure-stamp
843         dh_testdir
844         touch build-stamp
845
846 clean:
847         dh_testdir
848         dh_testroot
849         rm -f build-stamp configure-stamp
850         dh_clean
851
852 install: build
853         dh_testdir
854         dh_testroot
855         dh_clean -k
856         dh_installdirs
857
858         # ======================================================
859         #$(MAKE) DESTDIR="$(CURDIR)/debian/%(name)s" install
860         mkdir -p "$(CURDIR)/debian/%(name)s"
861
862         %(rules)s
863         # ======================================================
864
865 # Build architecture-independent files here.
866 binary-indep: build install
867 # We have nothing to do by default.
868
869 # Build architecture-dependent files here.
870 binary-arch: build install
871         dh_testdir
872         dh_testroot
873         dh_installchangelogs debian/changelog
874         dh_installdocs
875         dh_installexamples
876 #       dh_install
877 #       dh_installmenu
878 #       dh_installdebconf
879 #       dh_installlogrotate
880 #       dh_installemacsen
881 #       dh_installpam
882 #       dh_installmime
883 #       dh_python
884 #       dh_installinit
885 #       dh_installcron
886 #       dh_installinfo
887         dh_installman
888         dh_link
889         dh_strip
890         dh_compress
891         dh_fixperms
892 #       dh_perl
893 #       dh_makeshlibs
894         dh_installdeb
895         dh_shlibdeps
896         dh_gencontrol
897         dh_md5sums
898         dh_builddeb
899
900 binary: binary-indep binary-arch
901 .PHONY: build clean binary-indep binary-arch binary install configure
902 """ % packageContents
903             open(os.path.join(DEBIAN, "rules"), "w").write(txt)
904             os.chmod(os.path.join(DEBIAN, "rules"), 0755)
905
906             ###########################################################################
907             ###########################################################################
908             ###########################################################################
909
910             generatedFiles = []
911
912             if build:
913                 #http://www.debian.org/doc/manuals/maint-guide/ch-build.fr.html
914                 ret = os.system('cd "%(DEST)s"; dpkg-buildpackage -tc -rfakeroot -us -uc' % packageContents)
915                 if ret != 0:
916                     raise Py2debException("buildpackage failed (see output)")
917
918                 l=glob("%(TEMP)s/%(name)s*.deb" % packageContents)
919                 if len(l) != 1:
920                     raise Py2debException("didn't find builded deb")
921
922                 tdeb = l[0]
923                 deb = os.path.basename(tdeb)
924                 shutil.move(tdeb, deb)
925
926                 generatedFiles = [deb, ]
927
928                 if rpm:
929                     rpmFilename = deb2rpm(deb)
930                     generatedFiles.append(rpmFilename)
931
932                 if src:
933                     tarFilename = py2src(TEMP, name)
934                     generatedFiles.append(tarFilename)
935
936             if tar:
937                 tarFilename = py2tar(DEST, TEMP, name, version)
938                 generatedFiles.append(tarFilename)
939
940             if dsc:
941                 dscFilename = py2dsc(TEMP, name, version, depends, author, mail, arch)
942                 generatedFiles.append(dscFilename)
943
944             if changes:
945                 changesFilenames = py2changes(packageContents)
946                 generatedFiles.extend(changesFilenames)
947
948             return generatedFiles
949
950         #~ except Exception,m:
951             #~ raise Py2debException("build error :"+str(m))
952
953         finally:
954             if Py2deb.clear:
955                 shutil.rmtree(TEMP)
956
957
958 if __name__ == "__main__":
959     try:
960         os.chdir(os.path.dirname(sys.argv[0]))
961     except:
962         pass
963
964     p=Py2deb("python-py2deb")
965     p.description="Generate simple deb(/rpm/tgz) from python (2.4, 2.5 and 2.6)"
966     p.url = "http://www.manatlan.com/page/py2deb"
967     p.author=__author__
968     p.mail=__mail__
969     p.depends = "dpkg-dev, fakeroot, alien, python"
970     p.section="python"
971     p["/usr/lib/python2.6/dist-packages"] = ["py2deb.py", ]
972     p["/usr/lib/python2.5/site-packages"] = ["py2deb.py", ]
973     p["/usr/lib/python2.4/site-packages"] = ["py2deb.py", ]
974     #~ p.postinstall = "s.py"
975     #~ p.preinstall = "s.py"
976     #~ p.postremove = "s.py"
977     #~ p.preremove = "s.py"
978     print p
979     print p.generate(__version__, changelog = __doc__, src=True)