1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
# Copyright 2009 Didier Roche
3
# Copyright 2010 Tony Byrne
5
# This file is part of Quickly ubuntu-application template
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.
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.
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/>.
24
from launchpadlib.errors import HTTPError # pylint: disable=E0611
27
from quickly import configurationhandler
28
from quickly import launchpadaccess
29
from internal import quicklyutils
30
from quickly import templatetools
33
from gettext import gettext as _
36
gettext.textdomain('quickly')
38
class ppa_not_found(Exception):
40
class not_ppa_owner(Exception):
42
class user_team_not_found(Exception):
44
class invalid_versionning_scheme(Exception):
45
def __init__(self, msg):
49
class invalid_version_in_setup(Exception):
50
def __init__(self, msg):
60
def _continue_if_errors(err_output, warn_output, return_code,
61
ask_on_warn_or_error):
62
"""print existing error and warning"""
65
print #finish the current line
66
print ('----------------------------------')
67
print _('Command returned some ERRORS:')
68
print ('----------------------------------')
69
print ('\n'.join(err_output))
70
print ('----------------------------------')
72
# seek if not uneeded warning (noise from DistUtilsExtra.auto)
73
# line following the warning should be " …"
75
for line in warn_output:
76
if (re.match(".*not recognized by DistUtilsExtra.auto.*", line)):
78
if not re.match(' [^ ].*', warn_output[line_number + 1]):
79
warn_output.remove(line)
82
warn_output.remove(line)
85
# if still something, print it
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]: "):
99
def _filter_out(line, output_domain, err_output, warn_output):
100
'''filter output dispatching right domain'''
103
output_domain = DomainLevel.ERROR
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)
127
sys.stdout.write('.')
128
return (output_domain, err_output, warn_output)
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'''
134
if templatetools.in_verbose_mode():
135
return(subprocess.call(command))
137
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
138
stderr=subprocess.PIPE)
139
stdout_domain = DomainLevel.NONE
140
stderr_domain = DomainLevel.NONE
144
line_stdout = proc.stdout.readline().rstrip()
145
line_stderr = proc.stderr.readline().rstrip()
148
(stderr_domain, err_output, warn_output) = _filter_out(line_stderr, stderr_domain, err_output, warn_output)
151
# don't replace by if proc.poll() as the output can be empty
152
if proc.poll() != None:
156
(stdout_domain, err_output, warn_output) = _filter_out(line_stdout, stdout_domain, err_output, warn_output)
158
return(_continue_if_errors(err_output, warn_output, proc.returncode,
159
ask_on_warn_or_error))
161
def update_metadata():
162
# See https://wiki.ubuntu.com/PostReleaseApps/Metadata for details
165
project_name = configurationhandler.project_config['project']
167
# Grab name and category from desktop file
168
with open('%s.desktop.in' % project_name, 'r') as f:
171
match = re.search('\n_?Name=(.*)\n', desktop)
172
if match is not None:
173
metadata.append('XB-AppName: %s' % match.group(1))
175
match = re.search('\nCategories=(.*)\n', desktop)
176
if match is not None:
177
metadata.append('XB-Category: %s' % match.group(1))
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))
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):
198
with open('debian/rules', 'r') as f:
200
if contents and re.search('dpkg-distaddfile %s.svg' % project_name, contents) is None:
202
override_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)
209
metadata.append('XB-Icon: %s.svg' % project_name)
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')
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)
223
def get_forced_dependencies():
226
# check for yelp usage
227
if subprocess.call(["grep", "-rq", "['\"]ghelp:", "."]) == 0:
232
def updatepackaging(changelog=None, no_changelog=False, installopt=False):
233
"""create or update a package using python-mkdebian.
235
Commit after the first packaging creation"""
239
command = ['python-mkdebian']
240
version = get_python_mkdebian_version()
242
command.append('--force-control=full')
244
command.append('--force-control')
246
command.append("--force-copyright")
247
command.append("--force-rules")
249
command.append("--no-changelog")
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()
258
dependencies.extend([elem.strip() for elem
259
in configurationhandler.project_config['dependencies'].split(',')
263
for dep in dependencies:
264
command.extend(["--dependency", dep])
266
distribution = configurationhandler.project_config['target_distribution']
267
command.extend(["--distribution", distribution])
269
pass # Distribution has not been set by user, let python-mkdebian decide what it should be
272
return_code = _exec_and_log_errors(command, True)
274
print _("An error has occurred when creating debian packaging")
280
print _("Ubuntu packaging created in debian/")
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)
289
if re.match('(.|\n)*unknown:\n.*debian/(.|\n)*', bzr_status):
290
return_code = filter_exec_command(["bzr", "add"])
292
return_code = filter_exec_command(["bzr", "commit", "-m", 'Creating ubuntu package'])
297
def filter_exec_command(command):
298
''' Build either a source or a binary package'''
300
return(_exec_and_log_errors(command, False))
303
def shell_complete_ppa(ppa_to_complete):
304
''' Complete from available ppas '''
306
# connect to LP and get ppa to complete
308
launchpad = launchpadaccess.initialize_lpi(False)
309
except launchpadaccess.launchpad_connection_error:
314
(ppa_user, ppa_name) = get_ppa_parameters(launchpad, ppa_to_complete)
315
except user_team_not_found:
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")]
328
available_ppas.append(elem.name + '/')
329
return available_ppas
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 '''
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
339
lp_ppa_user = launchpad.people[ppa_user_name]
340
if lp_ppa_user.name == launchpad.me.name:
341
ppa_user = launchpad.me
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]
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)
352
ppa_user = launchpad.me
353
ppa_name = full_ppa_name
354
return(ppa_user, ppa_name)
356
def choose_ppa(launchpad, ppa_name=None):
357
'''Look for right ppa parameters where to push the package'''
360
if not configurationhandler.project_config:
361
configurationhandler.loadConfig()
363
(ppa_user, ppa_name) = get_ppa_parameters(launchpad, configurationhandler.project_config['ppa'])
365
ppa_user = launchpad.me
366
if (launchpadaccess.lp_server == "staging"):
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'))
376
def push_to_ppa(dput_ppa_name, changes_file, keyid=None):
377
""" Push some code to a ppa """
379
# creating local binary package
380
buildcommand = ["dpkg-buildpackage", "-S", "-I.bzr"]
382
buildcommand.append("-k%s" % keyid)
383
return_code = filter_exec_command(buildcommand)
385
print _("ERROR: an error occurred during source package creation")
387
# now, pushing it to launchpad personal ppa (or team later)
388
return_code = subprocess.call(["dput", dput_ppa_name, changes_file])
390
print _("ERROR: an error occurred during source upload to launchpad")
394
def get_all_ppas(launchpad, lp_team_or_user):
395
""" get all from a team or users
397
Return list of tuples (ppa_name, ppa_display_name)"""
400
for ppa in lp_team_or_user.ppas:
401
ppa_list.append((ppa.name, ppa.displayname))
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
407
return formated ppaname (not display name)"""
409
# check that the owner really has this ppa:
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:
416
raise ppa_not_found('ppa:%s:%s' % (lp_team_or_user.name, ppa_name.encode('UTF-8')))
417
return(current_ppa_name)
419
def updateversion(proposed_version=None, sharing=False):
420
'''Update versioning with year.month, handling intermediate release'''
423
# check manual versioning is correct
425
for number in proposed_version.split('.'):
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
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)
440
# sharing only add -publicX to last release, no other update, no bumping
442
splitted_release_version = old_version.split("-public")
443
if len(splitted_release_version) > 1:
445
share_version = float(splitted_release_version[1])
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))
454
new_version = old_version + "-public1"
456
# automatically version to year.month(.subversion)
458
base_version = datetime.datetime.now().strftime("%y.%m")
459
if base_version in old_version:
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]
465
minor_version = float(minor_version)
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") \
471
raise invalid_versionning_scheme(msg)
472
new_version = base_version + '.' + str(int(minor_version + 1))
475
# no minor version, bump to first one (be careful,
476
# old_version may contain -publicX)
477
new_version = base_version + '.1'
481
new_version = base_version
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, {})