~angeloc/quickly-ubuntu-qt-template/trunk

« back to all changes in this revision

Viewing changes to ubuntu-application-qt/internal/packaging.py

  • Committer: angeloc
  • Date: 2012-05-24 16:39:56 UTC
  • Revision ID: angelo.compagnucci@gmail.com-20120524163956-ijk2isvrh9kn832e
Initial code release.

* Templates are ready, there are minor fixing to do

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
 
2
# Copyright 2009 Didier Roche
 
3
# Copyright 2010 Tony Byrne
 
4
#
 
5
# This file is part of Quickly ubuntu-application template
 
6
#
 
7
#This program is free software: you can redistribute it and/or modify it
 
8
#under the terms of the GNU General Public License version 3, as published
 
9
#by the Free Software Foundation.
 
10
 
 
11
#This program is distributed in the hope that it will be useful, but
 
12
#WITHOUT ANY WARRANTY; without even the implied warranties of
 
13
#MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 
14
#PURPOSE.  See the GNU General Public License for more details.
 
15
 
 
16
#You should have received a copy of the GNU General Public License along
 
17
#with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
 
 
19
import datetime
 
20
import re
 
21
import subprocess
 
22
import sys
 
23
import os
 
24
from launchpadlib.errors import HTTPError # pylint: disable=E0611
 
25
 
 
26
 
 
27
from quickly import configurationhandler
 
28
from quickly import launchpadaccess
 
29
from internal import quicklyutils
 
30
from quickly import templatetools
 
31
 
 
32
import gettext
 
33
from gettext import gettext as _
 
34
 
 
35
#set domain text
 
36
gettext.textdomain('quickly')
 
37
 
 
38
class ppa_not_found(Exception):
 
39
    pass
 
40
class not_ppa_owner(Exception):
 
41
    pass
 
42
class user_team_not_found(Exception):
 
43
    pass
 
44
class invalid_versionning_scheme(Exception):
 
45
    def __init__(self, msg):
 
46
        self.msg = msg
 
47
    def __str__(self):
 
48
        return repr(self.msg)
 
49
class invalid_version_in_setup(Exception):
 
50
    def __init__(self, msg):
 
51
        self.msg = msg
 
52
    def __str__(self):
 
53
        return repr(self.msg)
 
54
 
 
55
class DomainLevel:
 
56
    NONE=0
 
57
    WARNING=1
 
58
    ERROR=2
 
59
 
 
60
def _continue_if_errors(err_output, warn_output, return_code,
 
61
                       ask_on_warn_or_error):
 
62
    """print existing error and warning"""
 
63
 
 
64
    if err_output:
 
65
        print #finish the current line
 
66
        print ('----------------------------------')
 
67
        print _('Command returned some ERRORS:')
 
68
        print ('----------------------------------')
 
69
        print ('\n'.join(err_output))
 
70
        print ('----------------------------------')
 
71
    if warn_output:
 
72
        # seek if not uneeded warning (noise from DistUtilsExtra.auto)
 
73
        # line following the warning should be "  …"
 
74
        line_number = 0
 
75
        for line in warn_output:
 
76
            if (re.match(".*not recognized by DistUtilsExtra.auto.*", line)):
 
77
                try:
 
78
                    if not re.match('  [^ ].*',  warn_output[line_number + 1]):
 
79
                        warn_output.remove(line)
 
80
                        line_number -= 1
 
81
                except IndexError:
 
82
                    warn_output.remove(line)
 
83
                    line_number -= 1  
 
84
            line_number += 1
 
85
        # if still something, print it     
 
86
        if warn_output:
 
87
            if not err_output:
 
88
                print #finish the current line
 
89
            print _('Command returned some WARNINGS:')
 
90
            print ('----------------------------------')
 
91
            print ('\n'.join(warn_output))
 
92
            print ('----------------------------------')
 
93
    if ((err_output or warn_output) and ask_on_warn_or_error
 
94
         and return_code == 0):
 
95
        if not 'y' in raw_input("Do you want to continue (this is not safe!)? y/[n]: "):
 
96
            return(4)
 
97
    return return_code
 
98
 
 
99
def _filter_out(line, output_domain, err_output, warn_output):
 
100
    '''filter output dispatching right domain'''
 
101
 
 
102
    if 'ERR' in line:
 
103
        output_domain = DomainLevel.ERROR
 
104
    elif 'WARN' in line:
 
105
        output_domain = DomainLevel.WARNING
 
106
    elif not line.startswith('  ') or line.startswith('   dh_'):
 
107
        output_domain = DomainLevel.NONE
 
108
        if '[not found]' in line:
 
109
            output_domain = DomainLevel.WARNING
 
110
    if output_domain == DomainLevel.ERROR:
 
111
        # only add once an error
 
112
        if not line in err_output:
 
113
                err_output.append(line)
 
114
    elif output_domain == DomainLevel.WARNING:
 
115
        # only add once a warning
 
116
        if not line in warn_output:
 
117
            # filter bad output from dpkg-buildpackage (on stderr) and p-d-e auto
 
118
            if not(re.match('  .*\.pot', line)
 
119
                   or re.match('  .*\.in', line)
 
120
                   or re.match(' dpkg-genchanges  >.*', line)
 
121
                   # python-mkdebian warns on help files
 
122
                   or re.match('  help/.*/.*', line)
 
123
                   # FIXME: this warning is temporary: should be withed in p-d-e
 
124
                   or re.match('.*XS-Python-Version and XB-Python-Version.*', line)):
 
125
                warn_output.append(line)
 
126
    else:
 
127
        sys.stdout.write('.')
 
128
    return (output_domain, err_output, warn_output)
 
129
 
 
130
 
 
131
def _exec_and_log_errors(command, ask_on_warn_or_error=False):
 
132
    '''exec the giving command and hide output if not in verbose mode'''
 
133
 
 
134
    if templatetools.in_verbose_mode():
 
135
        return(subprocess.call(command))
 
136
    else:
 
137
        proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
138
                                         stderr=subprocess.PIPE)
 
139
        stdout_domain = DomainLevel.NONE
 
140
        stderr_domain = DomainLevel.NONE
 
141
        err_output = []
 
142
        warn_output = []
 
143
        while True:
 
144
            line_stdout = proc.stdout.readline().rstrip()
 
145
            line_stderr = proc.stderr.readline().rstrip()
 
146
            # filter stderr
 
147
            if line_stderr:
 
148
                (stderr_domain, err_output, warn_output) = _filter_out(line_stderr, stderr_domain, err_output, warn_output)
 
149
 
 
150
            if not line_stdout:
 
151
                # don't replace by if proc.poll() as the output can be empty
 
152
                if proc.poll() != None:
 
153
                    break
 
154
            # filter stdout
 
155
            else:
 
156
                (stdout_domain, err_output, warn_output) = _filter_out(line_stdout, stdout_domain, err_output, warn_output)
 
157
 
 
158
        return(_continue_if_errors(err_output, warn_output, proc.returncode,
 
159
                                     ask_on_warn_or_error))
 
160
 
 
161
def update_metadata():
 
162
    # See https://wiki.ubuntu.com/PostReleaseApps/Metadata for details
 
163
 
 
164
    metadata = []
 
165
    project_name = configurationhandler.project_config['project']
 
166
 
 
167
    # Grab name and category from desktop file
 
168
    with open('%s.desktop.in' % project_name, 'r') as f:
 
169
        desktop = f.read()
 
170
 
 
171
        match = re.search('\n_?Name=(.*)\n', desktop)
 
172
        if match is not None:
 
173
            metadata.append('XB-AppName: %s' % match.group(1))
 
174
 
 
175
        match = re.search('\nCategories=(.*)\n', desktop)
 
176
        if match is not None:
 
177
            metadata.append('XB-Category: %s' % match.group(1))
 
178
 
 
179
    # Grab distribution for screenshot URLs from debian/changelog
 
180
    changelog = subprocess.Popen(['dpkg-parsechangelog'], stdout=subprocess.PIPE).communicate()[0]
 
181
    match = re.search('\nDistribution: (.*)\n', changelog)
 
182
    if match is not None:
 
183
        distribution = match.group(1)
 
184
        first_letter = project_name[0]
 
185
        urlbase = 'https://software-center.ubuntu.com/screenshots/%s' % first_letter
 
186
        metadata.append('XB-Screenshot-Url: %s/%s-%s.png' % (urlbase, project_name, distribution))
 
187
        metadata.append('XB-Thumbnail-Url: %s/%s-%s.thumb.png' % (urlbase, project_name, distribution))
 
188
 
 
189
    # Now ship the icon as part of the debian packaging
 
190
    icon_name = 'data/media/%s.svg' % project_name
 
191
    if not os.path.exists(icon_name):
 
192
        # Support pre-11.03.1 icon names
 
193
        icon_name = 'data/media/logo.svg'
 
194
        if not os.path.exists(icon_name):
 
195
            icon_name = None
 
196
    if icon_name:
 
197
        contents = ''
 
198
        with open('debian/rules', 'r') as f:
 
199
            contents = f.read()
 
200
        if contents and re.search('dpkg-distaddfile %s.svg' % project_name, contents) is None:
 
201
            contents += """
 
202
override_dh_install::
 
203
        dh_install
 
204
        cp %(icon_name)s ../%(project_name)s.svg
 
205
        dpkg-distaddfile %(project_name)s.svg raw-meta-data -""" % {
 
206
                'project_name': project_name, 'icon_name': icon_name}
 
207
            templatetools.set_file_contents('debian/rules', contents)
 
208
 
 
209
            metadata.append('XB-Icon: %s.svg' % project_name)
 
210
 
 
211
    # Prepend the start-match line, because update_file_content replaces it
 
212
    metadata.insert(0, 'XB-Python-Version: ${python:Versions}')
 
213
    templatetools.update_file_content('debian/control',
 
214
                                      'XB-Python-Version: ${python:Versions}',
 
215
                                      'Depends: ${misc:Depends},',
 
216
                                      '\n'.join(metadata) + '\n')
 
217
 
 
218
def get_python_mkdebian_version():
 
219
    proc = subprocess.Popen(["python-mkdebian", "--version"], stdout=subprocess.PIPE)
 
220
    version = proc.communicate()[0]
 
221
    return float(version)
 
222
 
 
223
def get_forced_dependencies():
 
224
    deps = []
 
225
 
 
226
    # check for yelp usage
 
227
    if subprocess.call(["grep", "-rq", "['\"]ghelp:", "."]) == 0:
 
228
        deps.append("yelp")
 
229
 
 
230
    return deps
 
231
 
 
232
def updatepackaging(changelog=None, no_changelog=False, installopt=False):
 
233
    """create or update a package using python-mkdebian.
 
234
 
 
235
    Commit after the first packaging creation"""
 
236
 
 
237
    if not changelog:
 
238
        changelog = []
 
239
    command = ['python-mkdebian']
 
240
    version = get_python_mkdebian_version()
 
241
    if version >= 2.28:
 
242
        command.append('--force-control=full')
 
243
    else:
 
244
        command.append('--force-control')
 
245
    if version > 2.22:
 
246
        command.append("--force-copyright")
 
247
        command.append("--force-rules")
 
248
    if no_changelog:
 
249
        command.append("--no-changelog")
 
250
    if installopt:
 
251
       command.append("--prefix=/opt/extras.ubuntu.com/%s" % configurationhandler.project_config['project'])
 
252
    for message in changelog:
 
253
        command.extend(["--changelog", message])
 
254
    if not configurationhandler.project_config:
 
255
        configurationhandler.loadConfig()
 
256
    dependencies = get_forced_dependencies()
 
257
    try:
 
258
        dependencies.extend([elem.strip() for elem
 
259
                             in configurationhandler.project_config['dependencies'].split(',')
 
260
                             if elem])
 
261
    except KeyError:
 
262
        pass
 
263
    for dep in dependencies:
 
264
        command.extend(["--dependency", dep])
 
265
    try:
 
266
        distribution = configurationhandler.project_config['target_distribution']
 
267
        command.extend(["--distribution", distribution])
 
268
    except KeyError:
 
269
        pass # Distribution has not been set by user, let python-mkdebian decide what it should be
 
270
 
 
271
 
 
272
    return_code = _exec_and_log_errors(command, True)
 
273
    if return_code != 0:
 
274
        print _("An error has occurred when creating debian packaging")
 
275
        return(return_code)
 
276
 
 
277
    if installopt:
 
278
        update_metadata()
 
279
 
 
280
    print _("Ubuntu packaging created in debian/")
 
281
 
 
282
    # check if first python-mkdebian (debian/ creation) to commit it
 
283
    # that means debian/ under unknown
 
284
    bzr_instance = subprocess.Popen(["bzr", "status"], stdout=subprocess.PIPE)
 
285
    bzr_status, err = bzr_instance.communicate()
 
286
    if bzr_instance.returncode != 0:
 
287
        return(bzr_instance.returncode)
 
288
 
 
289
    if re.match('(.|\n)*unknown:\n.*debian/(.|\n)*', bzr_status):
 
290
        return_code = filter_exec_command(["bzr", "add"])
 
291
        if return_code == 0:
 
292
            return_code = filter_exec_command(["bzr", "commit", "-m", 'Creating ubuntu package'])
 
293
 
 
294
    return(return_code)
 
295
 
 
296
 
 
297
def filter_exec_command(command):
 
298
    ''' Build either a source or a binary package'''
 
299
 
 
300
    return(_exec_and_log_errors(command, False))
 
301
 
 
302
 
 
303
def shell_complete_ppa(ppa_to_complete):
 
304
    ''' Complete from available ppas '''
 
305
 
 
306
    # connect to LP and get ppa to complete
 
307
    try:
 
308
        launchpad = launchpadaccess.initialize_lpi(False)
 
309
    except launchpadaccess.launchpad_connection_error:
 
310
        sys.exit(0)
 
311
    available_ppas = []
 
312
    if launchpad:
 
313
        try:
 
314
            (ppa_user, ppa_name) = get_ppa_parameters(launchpad, ppa_to_complete)
 
315
        except user_team_not_found:
 
316
            pass
 
317
        else:
 
318
            for current_ppa_name, current_ppa_displayname in get_all_ppas(launchpad, ppa_user):
 
319
                # print user/ppa form
 
320
                available_ppas.append("%s/%s" % (ppa_user.name, current_ppa_name))
 
321
                # if it's the user, print in addition just "ppa_name" syntax
 
322
                if ppa_user.name == launchpad.me.name:
 
323
                    available_ppas.append(current_ppa_name)
 
324
                # if we don't have provided a team, show all teams were we are member off
 
325
                if not '/' in ppa_to_complete:
 
326
                    team = [mem.team for mem in launchpad.me.memberships_details if mem.status in ("Approved", "Administrator")]
 
327
                    for elem in team:
 
328
                        available_ppas.append(elem.name + '/')
 
329
        return available_ppas
 
330
 
 
331
def get_ppa_parameters(launchpad, full_ppa_name):
 
332
    ''' Check if we can catch good parameters for specified ppa in form user/ppa or ppa '''
 
333
 
 
334
    if '/' in full_ppa_name:
 
335
        ppa_user_name = full_ppa_name.split('/')[0]
 
336
        ppa_name = full_ppa_name.split('/')[1]
 
337
        # check that we are in the team/or that we are the user
 
338
        try:
 
339
            lp_ppa_user = launchpad.people[ppa_user_name]
 
340
            if lp_ppa_user.name == launchpad.me.name:
 
341
                ppa_user = launchpad.me
 
342
            else:
 
343
                # check if we are a member of this team
 
344
                team = [mem.team for mem in launchpad.me.memberships_details if mem.status in ("Approved", "Administrator") and mem.team.name == ppa_user_name]
 
345
                if team:
 
346
                    ppa_user = team[0]
 
347
                else:
 
348
                    raise not_ppa_owner(ppa_user_name)
 
349
        except (KeyError, HTTPError): # launchpadlib may give 404 instead
 
350
            raise user_team_not_found(ppa_user_name)
 
351
    else:
 
352
        ppa_user = launchpad.me
 
353
        ppa_name = full_ppa_name
 
354
    return(ppa_user, ppa_name)
 
355
 
 
356
def choose_ppa(launchpad, ppa_name=None):
 
357
    '''Look for right ppa parameters where to push the package'''
 
358
 
 
359
    if not ppa_name:
 
360
        if not configurationhandler.project_config:
 
361
            configurationhandler.loadConfig()
 
362
        try:
 
363
            (ppa_user, ppa_name) = get_ppa_parameters(launchpad, configurationhandler.project_config['ppa'])
 
364
        except KeyError:
 
365
            ppa_user = launchpad.me
 
366
            if (launchpadaccess.lp_server == "staging"):
 
367
                ppa_name = 'staging'
 
368
            else: # default ppa
 
369
                ppa_name = 'ppa'
 
370
    else:
 
371
        (ppa_user, ppa_name) = get_ppa_parameters(launchpad, ppa_name)
 
372
    ppa_url = '%s/~%s/+archive/%s' % (launchpadaccess.LAUNCHPAD_URL, ppa_user.name, ppa_name)
 
373
    dput_ppa_name = 'ppa:%s/%s' % (ppa_user.name, ppa_name)
 
374
    return (ppa_user, ppa_name, dput_ppa_name, ppa_url.encode('UTF-8'))
 
375
 
 
376
def push_to_ppa(dput_ppa_name, changes_file, keyid=None):
 
377
    """ Push some code to a ppa """
 
378
 
 
379
    # creating local binary package
 
380
    buildcommand = ["dpkg-buildpackage", "-S", "-I.bzr"]
 
381
    if keyid:
 
382
        buildcommand.append("-k%s" % keyid)
 
383
    return_code = filter_exec_command(buildcommand)
 
384
    if return_code != 0:
 
385
        print _("ERROR: an error occurred during source package creation")
 
386
        return(return_code)
 
387
    # now, pushing it to launchpad personal ppa (or team later)
 
388
    return_code = subprocess.call(["dput", dput_ppa_name, changes_file])
 
389
    if return_code != 0:
 
390
        print _("ERROR: an error occurred during source upload to launchpad")
 
391
        return(return_code)
 
392
    return(0)
 
393
 
 
394
def get_all_ppas(launchpad, lp_team_or_user):
 
395
    """ get all from a team or users
 
396
 
 
397
    Return list of tuples (ppa_name, ppa_display_name)"""
 
398
 
 
399
    ppa_list = []
 
400
    for ppa in lp_team_or_user.ppas:
 
401
        ppa_list.append((ppa.name, ppa.displayname))
 
402
    return ppa_list
 
403
 
 
404
def check_and_return_ppaname(launchpad, lp_team_or_user, ppa_name):
 
405
    """ check whether ppa exists using its name or display name for the lp team or user
 
406
 
 
407
    return formated ppaname (not display name)"""
 
408
 
 
409
    # check that the owner really has this ppa:
 
410
    ppa_found = False
 
411
    for current_ppa_name, current_ppa_displayname in get_all_ppas(launchpad, lp_team_or_user):
 
412
        if current_ppa_name == ppa_name or current_ppa_displayname == ppa_name:
 
413
            ppa_found = True
 
414
            break
 
415
    if not ppa_found:
 
416
        raise ppa_not_found('ppa:%s:%s' % (lp_team_or_user.name, ppa_name.encode('UTF-8')))
 
417
    return(current_ppa_name)
 
418
 
 
419
def updateversion(proposed_version=None, sharing=False):
 
420
    '''Update versioning with year.month, handling intermediate release'''
 
421
 
 
422
    if proposed_version:
 
423
        # check manual versioning is correct
 
424
        try:
 
425
            for number in proposed_version.split('.'):
 
426
                float(number)
 
427
        except ValueError:
 
428
            msg = _("Release version specified in command arguments is not a " \
 
429
                    "valid version scheme like 'x(.y)(.z)'.")
 
430
            raise invalid_versionning_scheme(msg)
 
431
        new_version = proposed_version
 
432
    else:
 
433
        # get previous value
 
434
        try:
 
435
            old_version = quicklyutils.get_setup_value('version')
 
436
        except quicklyutils.cant_deal_with_setup_value:
 
437
            msg = _("No previous version found in setup.py. Put one please")
 
438
            raise invalid_version_in_setup(msg)
 
439
 
 
440
        # sharing only add -publicX to last release, no other update, no bumping
 
441
        if sharing:
 
442
            splitted_release_version = old_version.split("-public")
 
443
            if len(splitted_release_version) > 1:
 
444
                try:
 
445
                    share_version = float(splitted_release_version[1])
 
446
                except ValueError:
 
447
                    msg = _("Share version specified after -public in "\
 
448
                            "setup.py is not a valid number: %s") \
 
449
                            % splitted_release_version[1]
 
450
                    raise invalid_versionning_scheme(msg)
 
451
                new_version = splitted_release_version[0] + '-public' + \
 
452
                              str(int(share_version + 1))
 
453
            else:
 
454
                new_version = old_version + "-public1"
 
455
 
 
456
        # automatically version to year.month(.subversion)
 
457
        else:
 
458
            base_version = datetime.datetime.now().strftime("%y.%m")
 
459
            if base_version in old_version:
 
460
                try:
 
461
                    # try to get a minor version, removing -public if one
 
462
                    (year, month, minor_version) = old_version.split('.')
 
463
                    minor_version = minor_version.split('-public')[0]
 
464
                    try:
 
465
                        minor_version = float(minor_version)
 
466
                    except ValueError:
 
467
                        msg = _("Minor version specified in setup.py is not a " \
 
468
                                "valid number: %s. Fix this or specify a " \
 
469
                                "version as release command line argument") \
 
470
                                % minor_version
 
471
                        raise invalid_versionning_scheme(msg)
 
472
                    new_version = base_version + '.' + str(int(minor_version + 1))
 
473
 
 
474
                except ValueError:
 
475
                    # no minor version, bump to first one (be careful,
 
476
                    # old_version may contain -publicX)
 
477
                    new_version = base_version + '.1'
 
478
 
 
479
            else:
 
480
                # new year/month
 
481
                new_version = base_version
 
482
 
 
483
    # write release version to setup.py and update it in aboutdialog
 
484
    quicklyutils.set_setup_value('version', new_version)
 
485
    about_dialog_file_name = quicklyutils.get_about_file_name()
 
486
    if about_dialog_file_name:
 
487
        quicklyutils.change_xml_elem(about_dialog_file_name, "object/property",
 
488
                                     "name", "version", new_version, {})
 
489
 
 
490
    return new_version