Lint suggestions
[gonvert] / 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.prettyName = ""
505         self.description = description
506         self.upgradeDescription = ""
507         self.bugTracker = ""
508         self.license = license
509         self.depends = depends
510         self.recommends = ""
511         self.section = section
512         self.arch = arch
513         self.url = url
514         self.author = author
515         self.mail = mail
516         self.icon = ""
517         self.distribution = ""
518         self.respository = ""
519         self.urgency = "low"
520
521         self.preinstall = preinstall
522         self.postinstall = postinstall
523         self.preremove = preremove
524         self.postremove = postremove
525
526         self.__files={}
527
528     def __repr__(self):
529         name = self.name
530         license = self.license
531         description = self.description
532         depends = self.depends
533         recommends = self.recommends
534         section = self.section
535         arch = self.arch
536         url = self.url
537         author = self.author
538         mail = self.mail
539
540         preinstall = self.preinstall
541         postinstall = self.postinstall
542         preremove = self.preremove
543         postremove = self.postremove
544
545         paths=self.__files.keys()
546         paths.sort()
547         files=[]
548         for path in paths:
549             for file, nfile in self.__files[path]:
550                 #~ rfile=os.path.normpath(os.path.join(path, nfile))
551                 rfile=os.path.join(path, nfile)
552                 if nfile==file:
553                     files.append(rfile)
554                 else:
555                     files.append(rfile + " (%s)"%file)
556
557         files.sort()
558         files = "\n".join(files)
559
560
561         lscripts = [    preinstall and "preinst",
562                         postinstall and "postinst",
563                         preremove and "prerm",
564                         postremove and "postrm",
565                     ]
566         scripts = lscripts and ", ".join([i for i in lscripts if i]) or "None"
567         return """
568 ----------------------------------------------------------------------
569 NAME        : %(name)s
570 ----------------------------------------------------------------------
571 LICENSE     : %(license)s
572 URL         : %(url)s
573 AUTHOR      : %(author)s
574 MAIL        : %(mail)s
575 ----------------------------------------------------------------------
576 DEPENDS     : %(depends)s
577 RECOMMENDS  : %(recommends)s
578 ARCH        : %(arch)s
579 SECTION     : %(section)s
580 ----------------------------------------------------------------------
581 DESCRIPTION :
582 %(description)s
583 ----------------------------------------------------------------------
584 SCRIPTS : %(scripts)s
585 ----------------------------------------------------------------------
586 FILES :
587 %(files)s
588 """ % locals()
589
590     def generate(self, version, changelog="", rpm=False, src=False, build=True, tar=False, changes=False, dsc=False):
591         """ generate a deb of version 'version', with or without 'changelog', with or without a rpm
592             (in the current folder)
593             return a list of generated files
594         """
595         if not sum([len(i) for i in self.__files.values()])>0:
596             raise Py2debException("no files are defined")
597
598         if not changelog:
599             changelog="* no changelog"
600
601         name = self.name
602         description = self.description
603         license = self.license
604         depends = self.depends
605         recommends = self.recommends
606         section = self.section
607         arch = self.arch
608         url = self.url
609         distribution = self.distribution
610         repository = self.repository
611         urgency = self.urgency
612         author = self.author
613         mail = self.mail
614         files = self.__files
615         preinstall = self.preinstall
616         postinstall = self.postinstall
617         preremove = self.preremove
618         postremove = self.postremove
619
620         if section not in Py2deb.SECTIONS:
621             raise Py2debException("section '%s' is unknown (%s)" % (section, str(Py2deb.SECTIONS)))
622
623         if arch not in Py2deb.ARCHS:
624             raise Py2debException("arch '%s' is unknown (%s)"% (arch, str(Py2deb.ARCHS)))
625
626         if license not in Py2deb.LICENSES:
627             raise Py2debException("License '%s' is unknown (%s)" % (license, str(Py2deb.LICENSES)))
628
629         # create dates (buildDate, buildDateYear)
630         d=datetime.now()
631         buildDate=d.strftime("%a, %d %b %Y %H:%M:%S +0000")
632         buildDateYear=str(d.year)
633
634         #clean description (add a space before each next lines)
635         description=description.replace("\r", "").strip()
636         description = "\n ".join(description.split("\n"))
637
638         #clean changelog (add 2 spaces before each next lines)
639         changelog=changelog.replace("\r", "").strip()
640         changelog = "\n  ".join(changelog.split("\n"))
641
642         TEMP = ".py2deb_build_folder"
643         DEST = os.path.join(TEMP, name)
644         DEBIAN = os.path.join(DEST, "debian")
645
646         packageContents = locals()
647
648         # let's start the process
649         try:
650             shutil.rmtree(TEMP)
651         except:
652             pass
653
654         os.makedirs(DEBIAN)
655         try:
656             rules=[]
657             dirs=[]
658             for path in files:
659                 for ofile, nfile in files[path]:
660                     if os.path.isfile(ofile):
661                         # it's a file
662
663                         if ofile.startswith("/"): # if absolute path
664                             # we need to change dest
665                             dest=os.path.join(DEST, nfile)
666                         else:
667                             dest=os.path.join(DEST, ofile)
668
669                         # copy file to be packaged
670                         destDir = os.path.dirname(dest)
671                         if not os.path.isdir(destDir):
672                             os.makedirs(destDir)
673
674                         shutil.copy2(ofile, dest)
675
676                         ndir = os.path.join(path, os.path.dirname(nfile))
677                         nname = os.path.basename(nfile)
678
679                         # make a line RULES to be sure the destination folder is created
680                         # and one for copying the file
681                         fpath = "/".join(["$(CURDIR)", "debian", name+ndir])
682                         rules.append('mkdir -p "%s"' % fpath)
683                         rules.append('cp -a "%s" "%s"' % (ofile, os.path.join(fpath, nname)))
684
685                         # append a dir
686                         dirs.append(ndir)
687
688                     else:
689                         raise Py2debException("unknown file '' "%ofile) # shouldn't be raised (because controlled before)
690
691             # make rules right
692             rules= "\n\t".join(rules) + "\n"
693             packageContents["rules"] = rules
694
695             # make dirs right
696             dirs= [i[1:] for i in set(dirs)]
697             dirs.sort()
698
699             #==========================================================================
700             # CREATE debian/dirs
701             #==========================================================================
702             open(os.path.join(DEBIAN, "dirs"), "w").write("\n".join(dirs))
703
704             #==========================================================================
705             # CREATE debian/changelog
706             #==========================================================================
707             clog="""%(name)s (%(version)s) stable; urgency=low
708
709   %(changelog)s
710
711  -- %(author)s <%(mail)s>  %(buildDate)s
712 """ % packageContents
713
714             open(os.path.join(DEBIAN, "changelog"), "w").write(clog)
715
716             #==========================================================================
717             #Create pre/post install/remove
718             #==========================================================================
719             def mkscript(name, dest):
720                 if name and name.strip()!="":
721                     if os.path.isfile(name):    # it's a file
722                         content = file(name).read()
723                     else:   # it's a script
724                         content = name
725                     open(os.path.join(DEBIAN, dest), "w").write(content)
726
727             mkscript(preinstall, "preinst")
728             mkscript(postinstall, "postinst")
729             mkscript(preremove, "prerm")
730             mkscript(postremove, "postrm")
731
732
733             #==========================================================================
734             # CREATE debian/compat
735             #==========================================================================
736             open(os.path.join(DEBIAN, "compat"), "w").write("5\n")
737
738             #==========================================================================
739             # CREATE debian/control
740             #==========================================================================
741             generalParagraphFields = [
742                 "Source: %(name)s",
743                 "Maintainer: %(author)s <%(mail)s>",
744                 "Section: %(section)s",
745                 "Priority: extra",
746                 "Build-Depends: debhelper (>= 5)",
747                 "Standards-Version: 3.7.2",
748             ]
749
750             specificParagraphFields = [
751                 "Package: %(name)s",
752                 "Architecture: %(arch)s",
753                 "Depends: %(depends)s",
754                 "Recommends: %(recommends)s",
755                 "Description: %(description)s",
756             ]
757
758             if self.prettyName:
759                 prettyName = "XSBC-Maemo-Display-Name: %s" % self.prettyName.strip()
760                 specificParagraphFields.append("\n  ".join(prettyName.split("\n")))
761
762             if self.bugTracker:
763                 bugTracker = "XSBC-Bugtracker: %s" % self.bugTracker.strip()
764                 specificParagraphFields.append("\n  ".join(bugTracker.split("\n")))
765
766             if self.upgradeDescription:
767                 upgradeDescription = "XSBC-Maemo-Upgrade-Description: %s" % self.upgradeDescription.strip()
768                 specificParagraphFields.append("\n  ".join(upgradeDescription.split("\n")))
769
770             if self.icon:
771                 f = open(self.icon, "rb")
772                 try:
773                     rawIcon = f.read()
774                 finally:
775                     f.close()
776                 uueIcon = base64.b64encode(rawIcon)
777                 uueIconLines = []
778                 for i, c in enumerate(uueIcon):
779                     if i % 60 == 0:
780                         uueIconLines.append("")
781                     uueIconLines[-1] += c
782                 uueIconLines[0:0] = ("XSBC-Maemo-Icon-26:", )
783                 specificParagraphFields.append("\n  ".join(uueIconLines))
784
785             generalParagraph = "\n".join(generalParagraphFields)
786             specificParagraph = "\n".join(specificParagraphFields)
787             controlContent = "\n\n".join((generalParagraph, specificParagraph)) % packageContents
788             open(os.path.join(DEBIAN, "control"), "w").write(controlContent)
789
790             #==========================================================================
791             # CREATE debian/copyright
792             #==========================================================================
793             packageContents["txtLicense"] = LICENSE_AGREEMENT[license]
794             packageContents["pv"] =__version__
795             txt="""This package was py2debianized(%(pv)s) by %(author)s <%(mail)s> on
796 %(buildDate)s.
797
798 It was downloaded from %(url)s
799
800 Upstream Author: %(author)s <%(mail)s>
801
802 Copyright: %(buildDateYear)s by %(author)s
803
804 License:
805
806 %(txtLicense)s
807
808 The Debian packaging is (C) %(buildDateYear)s, %(author)s <%(mail)s> and
809 is licensed under the GPL, see above.
810
811
812 # Please also look if there are files or directories which have a
813 # different copyright/license attached and list them here.
814 """ % packageContents
815             open(os.path.join(DEBIAN, "copyright"), "w").write(txt)
816
817             #==========================================================================
818             # CREATE debian/rules
819             #==========================================================================
820             txt="""#!/usr/bin/make -f
821 # -*- makefile -*-
822 # Sample debian/rules that uses debhelper.
823 # This file was originally written by Joey Hess and Craig Small.
824 # As a special exception, when this file is copied by dh-make into a
825 # dh-make output file, you may use that output file without restriction.
826 # This special exception was added by Craig Small in version 0.37 of dh-make.
827
828 # Uncomment this to turn on verbose mode.
829 #export DH_VERBOSE=1
830
831
832
833
834 CFLAGS = -Wall -g
835
836 ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS)))
837         CFLAGS += -O0
838 else
839         CFLAGS += -O2
840 endif
841
842 configure: configure-stamp
843 configure-stamp:
844         dh_testdir
845         # Add here commands to configure the package.
846
847         touch configure-stamp
848
849
850 build: build-stamp
851
852 build-stamp: configure-stamp
853         dh_testdir
854         touch build-stamp
855
856 clean:
857         dh_testdir
858         dh_testroot
859         rm -f build-stamp configure-stamp
860         dh_clean
861
862 install: build
863         dh_testdir
864         dh_testroot
865         dh_clean -k
866         dh_installdirs
867
868         # ======================================================
869         #$(MAKE) DESTDIR="$(CURDIR)/debian/%(name)s" install
870         mkdir -p "$(CURDIR)/debian/%(name)s"
871
872         %(rules)s
873         # ======================================================
874
875 # Build architecture-independent files here.
876 binary-indep: build install
877 # We have nothing to do by default.
878
879 # Build architecture-dependent files here.
880 binary-arch: build install
881         dh_testdir
882         dh_testroot
883         dh_installchangelogs debian/changelog
884         dh_installdocs
885         dh_installexamples
886 #       dh_install
887 #       dh_installmenu
888 #       dh_installdebconf
889 #       dh_installlogrotate
890 #       dh_installemacsen
891 #       dh_installpam
892 #       dh_installmime
893 #       dh_python
894 #       dh_installinit
895 #       dh_installcron
896 #       dh_installinfo
897         dh_installman
898         dh_link
899         dh_strip
900         dh_compress
901         dh_fixperms
902 #       dh_perl
903 #       dh_makeshlibs
904         dh_installdeb
905         dh_shlibdeps
906         dh_gencontrol
907         dh_md5sums
908         dh_builddeb
909
910 binary: binary-indep binary-arch
911 .PHONY: build clean binary-indep binary-arch binary install configure
912 """ % packageContents
913             open(os.path.join(DEBIAN, "rules"), "w").write(txt)
914             os.chmod(os.path.join(DEBIAN, "rules"), 0755)
915
916             ###########################################################################
917             ###########################################################################
918             ###########################################################################
919
920             generatedFiles = []
921
922             if build:
923                 #http://www.debian.org/doc/manuals/maint-guide/ch-build.fr.html
924                 ret = os.system('cd "%(DEST)s"; dpkg-buildpackage -tc -rfakeroot -us -uc' % packageContents)
925                 if ret != 0:
926                     raise Py2debException("buildpackage failed (see output)")
927
928                 l=glob("%(TEMP)s/%(name)s*.deb" % packageContents)
929                 if len(l) != 1:
930                     raise Py2debException("didn't find builded deb")
931
932                 tdeb = l[0]
933                 deb = os.path.basename(tdeb)
934                 shutil.move(tdeb, deb)
935
936                 generatedFiles = [deb, ]
937
938                 if rpm:
939                     rpmFilename = deb2rpm(deb)
940                     generatedFiles.append(rpmFilename)
941
942                 if src:
943                     tarFilename = py2src(TEMP, name)
944                     generatedFiles.append(tarFilename)
945
946             if tar:
947                 tarFilename = py2tar(DEST, TEMP, name, version)
948                 generatedFiles.append(tarFilename)
949
950             if dsc:
951                 dscFilename = py2dsc(TEMP, name, version, depends, author, mail, arch)
952                 generatedFiles.append(dscFilename)
953
954             if changes:
955                 changesFilenames = py2changes(packageContents)
956                 generatedFiles.extend(changesFilenames)
957
958             return generatedFiles
959
960         #~ except Exception,m:
961             #~ raise Py2debException("build error :"+str(m))
962
963         finally:
964             if Py2deb.clear:
965                 shutil.rmtree(TEMP)
966
967
968 if __name__ == "__main__":
969     try:
970         os.chdir(os.path.dirname(sys.argv[0]))
971     except:
972         pass
973
974     p=Py2deb("python-py2deb")
975     p.description="Generate simple deb(/rpm/tgz) from python (2.4, 2.5 and 2.6)"
976     p.url = "http://www.manatlan.com/page/py2deb"
977     p.author=__author__
978     p.mail=__mail__
979     p.depends = "dpkg-dev, fakeroot, alien, python"
980     p.section="python"
981     p["/usr/lib/python2.6/dist-packages"] = ["py2deb.py", ]
982     p["/usr/lib/python2.5/site-packages"] = ["py2deb.py", ]
983     p["/usr/lib/python2.4/site-packages"] = ["py2deb.py", ]
984     #~ p.postinstall = "s.py"
985     #~ p.preinstall = "s.py"
986     #~ p.postremove = "s.py"
987     #~ p.preremove = "s.py"
988     print p
989     print p.generate(__version__, changelog = __doc__, src=True)