~mordred/+junk/quickly-drizzle-plugin

« back to all changes in this revision

Viewing changes to internal/packaging.py

  • Committer: Monty Taylor
  • Date: 2010-05-08 23:11:11 UTC
  • Revision ID: mordred@inaugust.com-20100508231111-mmohbj520o43e4sl
woot.

Show diffs side-by-side

added added

removed removed

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