2
# has to be Python2 because there's no python3 bzrlib
4
import sys, argparse, os, time, codecs, json, urllib, urllib2, re, subprocess
5
from gi.repository import GLib
6
from bzrlib.plugin import load_plugins
8
from bzrlib.branch import Branch
10
class Project(object):
11
def __init__(self, rootfolder):
12
self.rootfolder = rootfolder
14
def add_to_import_path(self, component_type):
17
def copy_branch_folder_contents_to(self, branch, tree, folder, destfolder):
18
# Ensure destfolder exists
20
os.makedirs(destfolder)
26
# Copy immediate file children of folder to it (not child folders or their contents)
28
for path, versioned, kind, file_id, inventory_entry in tree.list_files(from_dir=folder, recursive=False):
30
fp = open(os.path.join(destfolder, path), "wb")
31
fp.write(tree.get_file_text(file_id))
33
copied_files.append(path)
36
def install_component_from_branch_folder(self, branch, tree, folder, component, component_type):
37
if component_type == "qml":
38
destfolder = os.path.join(self.rootfolder, "ubuntu_component_store",
39
component["name"] + "." + component["version"])
40
copied_files = self.copy_branch_folder_contents_to(branch, tree, folder, destfolder)
41
# create qml qmldir file
43
for f in copied_files:
44
if f.endswith(".qml"):
45
qmlfiles.append("%s %s %s" % (os.path.splitext(f)[0], component["version"], f))
47
username, cname = component["name"].split("/")
48
cname = cname[0].upper() + cname[1:]
49
qmldir = ["module ubuntu_component_store.%s.%s" % (username, cname)]
51
qmldirfn = os.path.join(destfolder, "qmldir")
52
fp = codecs.open(qmldirfn, mode="w", encoding="utf8")
53
fp.write("\n".join(qmldir))
56
elif component_type == "binary":
57
fid = tree.path2id(folder)
59
raise Exception("Component has no '%s' folder" % (folder,))
61
for path, versioned, kind, file_id, inventory_entry in tree.list_files(from_dir=folder, recursive=False):
62
if kind == "directory":
65
destfolder = os.path.join(self.rootfolder, "lib", arch, "ubuntu_component_store",
66
component["name"] + "." + component["version"])
67
copied_files = self.copy_branch_folder_contents_to(branch, tree, folder + "/" + arch, destfolder)
68
# create binary qmldir file
70
for f in copied_files:
71
m = re.match("^lib(?P<libname>[^z.]+)\.so", f)
73
plugin_names.append(m.groupdict()["libname"])
75
username, cname = component["name"].split("/")
76
cname = cname[0].upper() + cname[1:]
77
qmldir = ["module ubuntu_component_store.%s.%s" % (username, cname)]
78
qmldir += ["plugin %s" % x for x in plugin_names]
79
qmldirfn = os.path.join(destfolder, "qmldir")
80
fp = codecs.open(qmldirfn, mode="w", encoding="utf8")
81
fp.write("\n".join(qmldir))
85
raise Exception("Unknown component type '%s'" % (component_type,))
87
def add_to_import_path(self, component_type):
88
raise NotImplementedError
90
def add_to_project_specific_copy_path(self, folder):
91
raise NotImplementedError
93
def add_to_project_specific_import_path(self, folder):
94
raise NotImplementedError
97
class PureQMLProject(Project):
98
def add_to_project_specific_import_path(self, folder):
99
"Add the passed folder to root/projname.qmlproject:importPaths"
100
projname = os.path.split(os.path.abspath(self.rootfolder))[1]
101
qmlprojfile = os.path.join(self.rootfolder, "%s.qmlproject" % projname)
102
if not os.path.exists(qmlprojfile):
103
raise Exception("Couldn't add import path to %s" % (qmlprojfile,))
104
fp = codecs.open(qmlprojfile, encoding="utf8")
107
ipre = "importPaths\s*:\s*\[(?P<paths>[^]]*)\]"
108
ip = re.search(ipre, data)
110
raise Exception("Couldn't add import path to %s" % (qmlprojfile,))
111
pathsline = ip.groupdict()["paths"]
112
paths = [re.sub('^"|"$', '', x.strip()) for x in pathsline.split(",")]
114
# this folder is already on the importPaths, so bail
117
pathsline += ', "%s"' % (folder,)
118
newpathline = "importPaths: [%s]" % pathsline
119
data = re.sub(ipre, newpathline, data)
120
fp = codecs.open(qmlprojfile, encoding="utf8", mode="w")
124
def add_to_project_specific_copy_path(self, folder):
125
"QML projects don't copy anything anywhere; they build in place. So this is not relevant."
128
def add_to_import_path(self, component_type):
129
if component_type == "binary":
130
# since this is a binary component, add the part for our arch to the Qt Creator import path
131
# FIXME it would be nice to not have to shell out for this
132
ourarch = subprocess.check_output(["dpkg-architecture", "-qDEB_BUILD_GNU_TYPE"]).strip()
133
self.add_to_project_specific_import_path("./lib/%s" % (ourarch,))
134
# FIXME may need an add_to_project_specific_copy_path here too
135
elif component_type == "qml":
136
# qml projects build in place anyway
140
class QMakeProject(Project):
141
def add_to_import_path(self, component_type):
142
if component_type == "binary":
143
print ("This is a qmake project.\n"
144
"You will need to ensure that all the ./lib/(arch)/ paths are added to "
145
"your import path when running the project, and that they are included "
146
"in the build process so they are copied into the build folder.")
147
elif component_type == "qml":
148
print ("This is a qmake project.\n"
149
"You will need to ensure that the ubuntu_component_store folder is "
150
"added to your import path when running the project, and that it is "
151
"included in the build process so it is copied into the build folder.")
153
class CMakeProject(QMakeProject):
154
def add_to_import_path(self, component_type):
155
if component_type == "binary":
156
print ("This is a cmake project.\n"
157
"You will need to ensure that all the ./lib/(arch)/ paths are added to "
158
"your import path when running the project, and that they are included "
159
"in the build process so they are copied into the build folder.")
160
elif component_type == "qml":
161
print ("This is a cmake project.\n"
162
"You will need to ensure that the ubuntu_component_store folder is "
163
"added to your import path when running the project, and that it is "
164
"included in the build process so it is copied into the build folder.")
167
"""Identify the current project type and return an appropriate wrapper.
169
Pure QML projects are in a folder Foo which has a Foo.qmlproject file in it.
170
qMake-based projects are in a folder Foo which has a Foo.pro file in it.
171
CMake-based projects are in a folder Foo which has CMakeLists.txt and that
172
file contains a project(Foo ...) line in it.
175
def get_class_for_folder(folder):
176
afolder = os.path.abspath(folder)
177
basename = os.path.split(afolder)[1]
178
cmakelistname = os.path.join(afolder, "CMakeLists.txt")
179
if os.path.exists(os.path.join(afolder, "%s.%s" % (basename, "qmlproject"))):
180
return PureQMLProject, afolder
181
elif os.path.exists(os.path.join(afolder, "%s.%s" % (basename, "pro"))):
182
return QMakeProject, afolder
183
elif os.path.exists(cmakelistname):
184
fp = codecs.open(cmakelistname, encoding="utf8")
185
lines = fp.read().split("\n")
188
m = re.match("^\s*project\s*\(\s*(?P<project>[^\s]+)((\s+[^\s]+)*\s*)\s*$", line)
190
if m.groupdict()["project"] == basename:
191
return CMakeProject, afolder
192
elif os.path.abspath(afolder) == "/":
193
# got to the root and found nothing
195
return get_class_for_folder(os.path.join(afolder, ".."))
197
project_class, root_folder = get_class_for_folder(os.curdir)
198
if not project_class:
199
raise Exception("Not in an Ubuntu SDK app: %s" % (os.curdir))
200
return project_class(root_folder)
202
class Repository(object):
203
def install_component_from_branch_folder(self, branch, tree, folder, component_type, component):
204
""" Download a thing from an LP folder and install it.
206
To install a component we need to:
208
2. put it in the correct place.
209
For pure QML components, the component will be one or more QML files, which
210
are in the branch folder that we are passed, and they get put in:
211
<project root>/ubuntu_component_store/username/componentname/
212
For binary components, the component will be a libFoo.so file for each arch,
213
and the branch folder we are passed should contain files <arch>/libFoo.so
214
for each arch. We then set up:
215
<project root>/lib/$arch/ubuntu_component_store/username/componentname/
216
for each arch and copy the appropriate libFoo.so into each.
217
3. Set up qmldir files.
218
Pure QML components get a qmldir file
219
<project root>/ubuntu_component_store/username/componentname/qmldir
222
RedRectangle 1.0 RedRectangle.qml
223
Binary components (libFoo.so, in component myname/BarPlugin) get a qmldir file
224
<project root>/lib/$arch/ubuntu_component_store/myname/BarPlugin/qmldir
226
module ubuntu_component_store.myname.BarPlugin
228
4. Make sure the components are on the import path.
229
Pure QML components do this automatically; they are on the import path because
230
their folder (ubuntu_component_store/username/componentname/) is in the project
231
root, and that's on the import path. Binary components do not, because the
232
<project root>/lib/$arch folder is *not* on the import path for running in Qt Creator.
233
(It *is* on the import path when packaged as a click and installed on Ubuntu;
234
the click-apparmor wrapper /usr/bin/aa-exec-click sets it.) So it's important to
235
ensure when installing a binary component that it's on the import path. This happens
236
in different ways depending on the type of project:
239
These have a projectname.qmlproject file in the project root, with an importPaths key. We add
240
"./lib/$arch" to it, for the arch that this machine is currently using. This is a
241
bit dodgy (adding arch-specific paths), but this importPaths key *already* contains
242
arch-specific paths (frex, "/usr/lib/x86_64-linux-gnu/qt5/qml") and so adding a project-specific
243
arch-specific path can't make that any worse.
252
project = get_project()
253
project.install_component_from_branch_folder(branch, tree, folder, component, component_type)
254
project.add_to_import_path(component_type)
256
def search_component_list(self, query, md):
257
matches = [{"componentname": x[0], "lcn": x[0].lower(),
258
"description": x[1].get("description", "(none)"),
259
"lcd": x[1].get("description", "").lower()}
265
if word in pkg["lcn"] or word in pkg["lcd"]:
270
class CuratedRepository(Repository):
271
UCS_BRANCH = "lp:~ubuntu-touch-community-dev/component-store/trunk.14.10"
272
COMPONENTS_ROOT = "curated-store/ComponentStore"
274
def __init__(self, args):
275
self.__refresh = args.refresh
277
def __get_branch(self):
278
b = Branch.open(self.UCS_BRANCH)
279
tree = b.basis_tree()
282
def __update_metadata_if_required(self):
283
cache_dir = GLib.get_user_cache_dir()
284
my_cache_dir = os.path.join(cache_dir, "ubuntu_component_store")
286
os.makedirs(my_cache_dir)
291
cached_curated_file = os.path.join(my_cache_dir, "ucs-curated-components-metadata.json")
293
branch, tree = self.__get_branch()
294
fetch_packages = False
296
fp = codecs.open(cached_curated_file, encoding="utf8")
297
component_metadata = json.load(fp)
298
if branch.revno() > component_metadata.get("cached_revno", 0):
299
fetch_packages = True
301
fetch_packages = True
304
fetch_packages = True
305
self.__refresh = False
308
component_metadata = {"cached_revno": branch.revno(), "components": {}}
310
for (path, versioned, kind, file_id, inventory_entry)
311
in tree.list_files(from_dir=self.COMPONENTS_ROOT)
312
if kind == "directory"
316
mdpath = "%s/%s/%s" % (self.COMPONENTS_ROOT, c, "ubuntu_component_store.json")
317
file_id = tree.path2id(mdpath)
318
mdjson = tree.get_file_text(file_id)
319
md = json.loads(mdjson)
320
component_metadata["components"][c] = md
322
print "(unable to get metadata for curated component %s)" % (c,)
323
fp = codecs.open(cached_curated_file, mode="w", encoding="utf8")
324
json.dump(component_metadata, fp)
326
return component_metadata
328
def install(self, cn):
329
print("(installing {0} from curated repository)".format(cn))
330
component_metadata = self.__update_metadata_if_required()
331
component = component_metadata["components"].get(cn)
333
raise Exception("Unable to find curated component '%s'." % (cn,))
334
b, tree = self.__get_branch()
335
component_path = "/".join([self.COMPONENTS_ROOT, cn])
336
file_id = tree.path2id(component_path)
337
if not file_id: # should not happen because it's in the metadata
338
raise Exception("Unable to find curated component '%s'." % (cn,))
339
# Curated components are all QML-only
340
component["name"] = "Curated/%s" % (component["name"],)
341
self.install_component_from_branch_folder(b, tree, component_path, "qml", component)
343
def update(self, cn):
344
raise NotImplementedError
346
def search(self, query):
347
component_metadata = self.__update_metadata_if_required()
348
return self.search_component_list(query, component_metadata.get("components", {}))
350
class CommunityRepository(Repository):
351
PACKAGES_FILE_BRANCH = "lp:~ubuntu-touch-community-dev/component-store/community-components-json"
352
PACKAGES_FILE_NAME = "community_components.json"
354
def __init__(self, args):
355
self.__refresh = args.refresh
357
def __update_packages_file_if_required(self):
358
cache_dir = GLib.get_user_cache_dir()
359
my_cache_dir = os.path.join(cache_dir, "ubuntu_component_store")
361
os.makedirs(my_cache_dir)
366
cached_packages_file = os.path.join(my_cache_dir, "ucs-packages-cache.json")
368
fetch_packages = False
370
age = time.time() - os.stat(cached_packages_file).st_mtime
371
if age > 3600: fetch_packages = True
373
fetch_packages = True
375
fetch_packages = True
376
self.__refresh = False
379
packages_data = self.__fetch_packages_file(cached_packages_file)
381
fp = codecs.open(cached_packages_file, encoding="utf8")
383
packages_data = json.load(fp)
385
raise Exception("Local package cache seems corrupt")
389
def __fetch_packages_file(self, cached_packages_file):
390
print "(getting packages file)"
391
b = Branch.open(self.PACKAGES_FILE_BRANCH)
392
tree = b.basis_tree()
393
file_id = tree.path2id(self.PACKAGES_FILE_NAME)
395
raise Exception("Unable to find the packages file.")
397
packages_data = tree.get_file_text(file_id)
399
raise Exception("Unable to read the packages file.")
401
packages_data = json.loads(packages_data)
403
raise Exception("Master packages file seems corrupt")
404
fp = codecs.open(cached_packages_file, mode="w", encoding="utf8")
405
json.dump(packages_data, fp)
409
def install(self, cn):
410
packages_data = self.__update_packages_file_if_required()
411
print("(installing {0} from community repository)".format(cn))
412
pkg = packages_data.get("components", {}).get(cn)
414
print("Package {0} not found".format(cn))
416
print("OK installing.")
417
b = Branch.open(pkg["lpurl"])
418
tree = b.repository.revision_tree(b.get_rev_id(pkg["revno"]))
419
if "qml" in pkg["type"]:
420
self.install_component_from_branch_folder(b, tree, "qml", "qml", pkg)
421
if "binary" in pkg["type"]:
422
self.install_component_from_branch_folder(b, tree, "qmllib", "binary", pkg)
424
def update(self, cn):
425
raise NotImplementedError
427
def search(self, query):
428
packages_data = self.__update_packages_file_if_required()
429
return self.search_component_list(query, packages_data.get("components", {}))
431
def submit(self, lpurl):
433
def chunk_read(response):
437
chunk = response.read(1)
441
lines = read.split("\n")
443
lines = [x.split(":", 1) for x in lines]
447
print "Error: %s" % x[1]
449
print "\n".join([x[1] for x in lines])
453
if not read.startswith("ok:"): return False
458
response = urllib2.urlopen('https://sil.pythonanywhere.com/submit', data=urllib.urlencode({"lpurl": lpurl}));
459
except urllib2.HTTPError, e:
460
print("Error while submitting:")
463
ok = chunk_read(response)
464
if not ok: sys.exit(1)
466
def get_repository_from_component_name(args):
467
if "/" in args.componentname:
468
return CommunityRepository(args)
470
return CuratedRepository(args)
472
def cmd_install(args):
473
get_repository_from_component_name(args).install(args.componentname)
474
def cmd_update(args):
475
get_repository_from_component_name(args).update(args.componentname)
476
def cmd_search(args):
477
print("Matching curated components")
478
print("===========================")
479
cu = CuratedRepository(args)
480
results = cu.search(args.query)
481
for result in results:
482
print(" {componentname:>30} {description}".format(**result))
484
print(" {0:>30}".format("(no matches)"))
486
print("Matching community components")
487
print("=============================")
488
co = CommunityRepository(args)
489
results = co.search(args.query)
490
for result in results:
491
print(" {componentname:>30} {description}".format(**result))
493
print(" {0:>30}".format("(no matches)"))
495
def cmd_submit(args):
496
print("(submitting to community repository)")
498
co = CommunityRepository(args)
499
co.submit(args.launchpadBranchURL)
501
if __name__ == "__main__":
502
parser = argparse.ArgumentParser(description="Install and manage third-party components into your Ubuntu SDK app")
503
subparsers = parser.add_subparsers(dest="subparser_name")
504
parser_install = subparsers.add_parser("install", help="Install a component into this app")
505
parser_install.add_argument("componentname", type=str,
506
help="ComponentName for curated components, developer/ComponentName for community components")
507
parser_install.add_argument("--refresh", action="store_true", help="Refresh the packages list")
508
parser_install.set_defaults(func=cmd_install)
509
parser_update = subparsers.add_parser("update", help="Update an installed component in this app")
510
parser_update.add_argument("componentname", type=str,
511
help="ComponentName for curated components, developer/ComponentName for community components")
512
parser_update.add_argument("--refresh", action="store_true", help="Refresh the packages list")
513
parser_update.set_defaults(func=cmd_update)
514
parser_search = subparsers.add_parser("search", help="Search names and descriptions of existing components")
515
parser_search.add_argument("query", type=str, nargs="*", help="Search terms")
516
parser_search.add_argument("--refresh", action="store_true", help="Refresh the packages list")
517
parser_search.set_defaults(func=cmd_search)
518
parser_submit = subparsers.add_parser("submit", help="Publish a component to the community store")
519
parser_submit.add_argument("launchpadBranchURL", type=str, help="lp:~username/project/branch")
520
parser_submit.set_defaults(func=cmd_submit)
521
parser_help = subparsers.add_parser("help", help="Help on subcommands")
522
parser_help.add_argument("subcommand", nargs="?", type=str)
524
args = parser.parse_args()
525
if args.subparser_name == "help":
527
parser.parse_args([args.subcommand, "-h"])
529
parser.parse_args(["-h"])