Merge lp:~diegosarmentero/unity-scope-click/open-after-install into lp:unity-scope-click
- open-after-install
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 170 |
Proposed branch: | lp:~diegosarmentero/unity-scope-click/open-after-install |
Merge into: | lp:unity-scope-click |
Prerequisite: | lp:~dobey/unity-scope-click/uninstall |
Diff against target: |
433 lines (+253/-9) 10 files modified
debian/control (+1/-0) scope/click/CMakeLists.txt (+5/-1) scope/click/index.cpp (+3/-3) scope/click/interface.cpp (+104/-0) scope/click/interface.h (+22/-0) scope/click/scope.cpp (+33/-4) scope/tests/CMakeLists.txt (+1/-0) scope/tests/click_interface_tool/CMakeLists.txt (+14/-0) scope/tests/click_interface_tool/click_interface_tool.cpp (+69/-0) scope/tests/test_index.cpp (+1/-1) |
To merge this branch: | bzr merge lp:~diegosarmentero/unity-scope-click/open-after-install |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Ubuntu One hackers | Pending | ||
Review via email: mp+207836@code.launchpad.net |
This proposal supersedes a proposal from 2014-02-23.
Commit message
Enable uninstall, fix launching apps right after installation.
Description of the change
- 175. By Diego Sarmentero
-
remove commented code for testing
- 176. By Diego Sarmentero
-
avoid unnecesary repetitions in the loop
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:175
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:176
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
- 177. By Diego Sarmentero
-
using url-dispatch to launch the new apps
- 178. By Diego Sarmentero
-
fix uninstall process
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:178
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
- 179. By Diego Sarmentero
-
using liburl-dispatcher
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:179
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
- 180. By Diego Sarmentero
-
typo in uninstall fixed
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:180
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'debian/control' |
2 | --- debian/control 2014-02-18 15:34:14 +0000 |
3 | +++ debian/control 2014-02-23 22:50:47 +0000 |
4 | @@ -20,6 +20,7 @@ |
5 | libubuntuoneauth-2.0-dev, |
6 | libunity-dev (>= 7.0.0), |
7 | libunity-scopes-dev (>= 0.3.1), |
8 | + liburl-dispatcher1-dev, |
9 | pkg-config, |
10 | python (>= 2.7), |
11 | valac, |
12 | |
13 | === modified file 'scope/click/CMakeLists.txt' |
14 | --- scope/click/CMakeLists.txt 2014-02-17 08:45:26 +0000 |
15 | +++ scope/click/CMakeLists.txt 2014-02-23 22:50:47 +0000 |
16 | @@ -1,6 +1,7 @@ |
17 | SET (CMAKE_INCLUDE_CURRENT_DIR ON) |
18 | SET (CMAKE_AUTOMOC ON) |
19 | find_package (Qt5Core REQUIRED) |
20 | +pkg_check_modules(LIBURL_DISPATCHER REQUIRED url-dispatcher-1) |
21 | |
22 | add_library(${SCOPE_LIB_UNVERSIONED} SHARED |
23 | download-manager.cpp |
24 | @@ -21,14 +22,17 @@ |
25 | configure_file(config.h.in config.h) |
26 | |
27 | include_directories (${CMAKE_BINARY_DIR}/scope) |
28 | +include_directories(${LIBURL_DISPATCHER_INCLUDE_DIRS}) |
29 | +link_directories(${LIBURL_DISPATCHER_LIBRARY_DIRS}) |
30 | |
31 | -qt5_use_modules (${SCOPE_LIB_UNVERSIONED} Network) |
32 | +qt5_use_modules (${SCOPE_LIB_UNVERSIONED} Network Gui) |
33 | |
34 | target_link_libraries (${SCOPE_LIB_UNVERSIONED} |
35 | ${UNITY_SCOPES_LDFLAGS} |
36 | ${UBUNTUONE_LDFLAGS} |
37 | ${UBUNTU_DOWNLOAD_MANAGER_CLIENT_LDFLAGS} |
38 | ${UBUNTU_DOWNLOAD_MANAGER_COMMON_LDFLAGS} |
39 | + ${LIBURL_DISPATCHER_LIBRARIES} |
40 | ) |
41 | |
42 | install( |
43 | |
44 | === modified file 'scope/click/index.cpp' |
45 | --- scope/click/index.cpp 2014-02-23 22:50:47 +0000 |
46 | +++ scope/click/index.cpp 2014-02-23 22:50:47 +0000 |
47 | @@ -75,7 +75,7 @@ |
48 | void PackageManager::uninstall(const Package& package, |
49 | std::function<void(int, std::string)> callback) |
50 | { |
51 | - std::string package_id = package.name + ";" + package.version + ";all;local;click"; |
52 | + std::string package_id = package.name + ";" + package.version + ";all;local:click"; |
53 | std::string command = "pkcon -p remove " + package_id; |
54 | execute_uninstall_command(command, callback); |
55 | } |
56 | @@ -89,7 +89,7 @@ |
57 | typedef void(QProcess::*QProcessError)(QProcess::ProcessError); |
58 | QObject::connect(process.data(), |
59 | static_cast<QProcessFinished>(&QProcess::finished), |
60 | - [&](int code, QProcess::ExitStatus status) { |
61 | + [process, callback](int code, QProcess::ExitStatus status) { |
62 | Q_UNUSED(status); |
63 | qDebug() << "command finished with exit code:" << code; |
64 | callback(code, process.data()->readAllStandardError().data()); |
65 | @@ -99,7 +99,7 @@ |
66 | } ); |
67 | QObject::connect(process.data(), |
68 | static_cast<QProcessError>(&QProcess::error), |
69 | - [&](QProcess::ProcessError error) { |
70 | + [process, callback](QProcess::ProcessError error) { |
71 | qCritical() << "error running command:" << error; |
72 | callback(-255 + error, process.data()->readAllStandardError().data()); |
73 | } ); |
74 | |
75 | === modified file 'scope/click/interface.cpp' |
76 | --- scope/click/interface.cpp 2014-02-23 22:50:47 +0000 |
77 | +++ scope/click/interface.cpp 2014-02-23 22:50:47 +0000 |
78 | @@ -29,6 +29,7 @@ |
79 | |
80 | #include <QDebug> |
81 | #include <QDir> |
82 | +#include <QProcess> |
83 | #include <QStandardPaths> |
84 | #include <QTimer> |
85 | |
86 | @@ -36,6 +37,11 @@ |
87 | #include <sys/stat.h> |
88 | #include <map> |
89 | |
90 | +#include <boost/property_tree/ptree.hpp> |
91 | +#include <boost/property_tree/json_parser.hpp> |
92 | +#include <boost/property_tree/exceptions.hpp> |
93 | +#include <boost/foreach.hpp> |
94 | + |
95 | #include <unity/UnityExceptions.h> |
96 | #include <unity/util/IniParser.h> |
97 | |
98 | @@ -231,4 +237,102 @@ |
99 | } |
100 | } |
101 | |
102 | + |
103 | +ManifestList manifest_list_from_json(const std::string& json) |
104 | +{ |
105 | + using namespace boost::property_tree; |
106 | + |
107 | + std::istringstream is(json); |
108 | + |
109 | + ptree pt; |
110 | + read_json(is, pt); |
111 | + |
112 | + ManifestList manifests; |
113 | + |
114 | + BOOST_FOREACH(ptree::value_type &v, pt) |
115 | + { |
116 | + assert(v.first.empty()); // array elements have no names |
117 | + auto node = v.second; |
118 | + std::string name = node.get<std::string>("name"); |
119 | + std::string version = node.get<std::string>("version"); |
120 | + std::string first_app_name; |
121 | + |
122 | + BOOST_FOREACH(ptree::value_type &sv, node.get_child("hooks")) |
123 | + { |
124 | + // FIXME: "primary app" for a package is not defined, we just |
125 | + // use first one here: |
126 | + first_app_name = sv.first; |
127 | + break; |
128 | + } |
129 | + qDebug() << "adding manifest: " << name.c_str() << version.c_str() << first_app_name.c_str(); |
130 | + |
131 | + manifests.push_back(Manifest(name, version, first_app_name)); |
132 | + } |
133 | + |
134 | + return manifests; |
135 | +} |
136 | + |
137 | +void Interface::get_manifests(std::function<void(ManifestList, ManifestError)> callback) |
138 | +{ |
139 | + QSharedPointer<QProcess> process(new QProcess()); |
140 | + typedef void(QProcess::*QProcessFinished)(int, QProcess::ExitStatus); |
141 | + typedef void(QProcess::*QProcessError)(QProcess::ProcessError); |
142 | + QObject::connect(process.data(), |
143 | + static_cast<QProcessFinished>(&QProcess::finished), |
144 | + [callback, process](int code, QProcess::ExitStatus /*status*/) { |
145 | + qDebug() << "manifest command finished with exit code:" << code; |
146 | + try { |
147 | + auto data = process.data()->readAllStandardOutput().data(); |
148 | + ManifestList manifests = manifest_list_from_json(data); |
149 | + qDebug() << "calling back "; |
150 | + callback(manifests, ManifestError::NoError); |
151 | + } |
152 | + catch ( ... ) { |
153 | + callback(ManifestList(), ManifestError::ParseError); |
154 | + } |
155 | + } ); |
156 | + |
157 | + QObject::connect(process.data(), |
158 | + static_cast<QProcessError>(&QProcess::error), |
159 | + [callback, process](QProcess::ProcessError error) { |
160 | + qCritical() << "error running command:" << error; |
161 | + callback(ManifestList(), ManifestError::CallError); |
162 | + } ); |
163 | + |
164 | + std::string command = "click list --manifest"; |
165 | + qDebug() << "Running command:" << command.c_str(); |
166 | + process->start(command.c_str()); |
167 | +} |
168 | + |
169 | +void Interface::get_dotdesktop_filename(const std::string &app_id, |
170 | + std::function<void(std::string, ManifestError)> callback) |
171 | +{ |
172 | + get_manifests([app_id, callback] (ManifestList manifests, ManifestError error) { |
173 | + qDebug() << "in get_dotdesktop_filename callback"; |
174 | + |
175 | + if (error != ManifestError::NoError){ |
176 | + callback(std::string("Internal Error"), error); |
177 | + return; |
178 | + } |
179 | + bool found = false; |
180 | + qDebug() << "in get_dotdesktop_filename callback"; |
181 | + |
182 | + |
183 | + for (auto manifest : manifests) { |
184 | + if (manifest.name != app_id) continue; |
185 | + found = true; |
186 | + std::string ddstr = manifest.name + "_" + manifest.first_app_name + "_" + manifest.version + ".desktop"; |
187 | + callback(ddstr, ManifestError::NoError); |
188 | + break; |
189 | + } |
190 | + if (!found) { |
191 | + qCritical() << "Warning: no manifest found for " << app_id.c_str(); |
192 | + callback(std::string("Not found"), ManifestError::CallError); |
193 | + } |
194 | + }); |
195 | +} |
196 | + |
197 | + |
198 | + |
199 | + |
200 | } // namespace click |
201 | |
202 | === modified file 'scope/click/interface.h' |
203 | --- scope/click/interface.h 2014-02-17 19:12:56 +0000 |
204 | +++ scope/click/interface.h 2014-02-23 22:50:47 +0000 |
205 | @@ -46,6 +46,25 @@ |
206 | // Hash map of desktop files that are not yet click packages |
207 | const std::unordered_set<std::string>& nonClickDesktopFiles(); |
208 | |
209 | +struct Manifest |
210 | +{ |
211 | + Manifest() = default; |
212 | + Manifest(std::string name, std::string version, std::string first_app_name) : |
213 | + name(name), version(version), first_app_name(first_app_name) |
214 | + { |
215 | + } |
216 | + virtual ~Manifest() = default; |
217 | + |
218 | + std::string name; |
219 | + std::string version; |
220 | + std::string first_app_name; |
221 | +}; |
222 | + |
223 | +enum class ManifestError {NoError, CallError, ParseError}; |
224 | +typedef std::list<Manifest> ManifestList; |
225 | + |
226 | +ManifestList manifest_list_from_json(const std::string& json); |
227 | + |
228 | class Interface |
229 | { |
230 | public: |
231 | @@ -61,6 +80,9 @@ |
232 | |
233 | static bool is_icon_identifier(const std::string &icon_id); |
234 | static std::string add_theme_scheme(const std::string &filename); |
235 | + static void get_manifests(std::function<void(ManifestList, ManifestError)> callback); |
236 | + static void get_dotdesktop_filename(const std::string &app_id, |
237 | + std::function<void(std::string filename, ManifestError)> callback); |
238 | private: |
239 | QSharedPointer<KeyFileLocator> keyFileLocator; |
240 | }; |
241 | |
242 | === modified file 'scope/click/scope.cpp' |
243 | --- scope/click/scope.cpp 2014-02-23 22:50:47 +0000 |
244 | +++ scope/click/scope.cpp 2014-02-23 22:50:47 +0000 |
245 | @@ -33,8 +33,22 @@ |
246 | #include "preview.h" |
247 | #include "webclient.h" |
248 | #include "network_access_manager.h" |
249 | +#include "key_file_locator.h" |
250 | +#include "interface.h" |
251 | |
252 | #include <QSharedPointer> |
253 | +#include <url-dispatcher.h> |
254 | + |
255 | +namespace |
256 | +{ |
257 | +click::Interface& clickInterfaceInstance() |
258 | +{ |
259 | + static QSharedPointer<click::KeyFileLocator> keyFileLocator(new click::KeyFileLocator()); |
260 | + static click::Interface iface(keyFileLocator); |
261 | + |
262 | + return iface; |
263 | +} |
264 | +} |
265 | |
266 | class ScopeActivation : public unity::scopes::ActivationBase |
267 | { |
268 | @@ -105,7 +119,7 @@ |
269 | if(metadict.count(click::Preview::Actions::DOWNLOAD_FAILED) != 0) { |
270 | return scopes::QueryBase::UPtr{new ErrorPreview(std::string("Download or install failed. Please try again."), |
271 | index, result)}; |
272 | - } else if (metadict.count(click::Preview::Actions::DOWNLOAD_COMPLETED) != 0 && |
273 | + } else if (metadict.count(click::Preview::Actions::DOWNLOAD_COMPLETED) != 0 || |
274 | metadict.count(click::Preview::Actions::CLOSE_PREVIEW) != 0) { |
275 | Preview* prev = new Preview(result.uri(), index, result); |
276 | prev->setPreview(click::Preview::Type::INSTALLED); |
277 | @@ -131,13 +145,29 @@ |
278 | return previewResult; |
279 | } |
280 | |
281 | -unity::scopes::ActivationBase::UPtr click::Scope::perform_action(unity::scopes::Result const& /*result*/, unity::scopes::ActionMetadata const& metadata, std::string const& /* widget_id */, std::string const& action_id) |
282 | +unity::scopes::ActivationBase::UPtr click::Scope::perform_action(unity::scopes::Result const& result, unity::scopes::ActionMetadata const& metadata, std::string const& /* widget_id */, std::string const& action_id) |
283 | { |
284 | auto activation = new ScopeActivation(); |
285 | qDebug() << "perform_action called with action_id" << QString().fromStdString(action_id); |
286 | |
287 | if (action_id == click::Preview::Actions::OPEN_CLICK) { |
288 | - activation->setStatus(unity::scopes::ActivationResponse::Status::NotHandled); |
289 | + QString app_url = QString::fromStdString(result.uri()); |
290 | + if (!app_url.startsWith("application:///")) { |
291 | + qt::core::world::enter_with_task([this, result] (qt::core::world::Environment& /*env*/) |
292 | + { |
293 | + clickInterfaceInstance().get_dotdesktop_filename(result["name"].get_string(), |
294 | + [] (std::string val, click::ManifestError error){ |
295 | + if (error == click::ManifestError::NoError) { |
296 | + std::string uri = "application:///" + val; |
297 | + url_dispatch_send(uri.c_str() , NULL, NULL); |
298 | + } |
299 | + } |
300 | + ); |
301 | + }); |
302 | + activation->setStatus(unity::scopes::ActivationResponse::Status::HideDash); |
303 | + } else { |
304 | + activation->setStatus(unity::scopes::ActivationResponse::Status::NotHandled); |
305 | + } |
306 | } else if (action_id == click::Preview::Actions::INSTALL_CLICK) { |
307 | std::string download_url = metadata.scope_data().get_dict()["download_url"].get_string(); |
308 | qDebug() << "the download url is: " << QString::fromStdString(download_url); |
309 | @@ -149,7 +179,6 @@ |
310 | } else if (action_id == click::Preview::Actions::DOWNLOAD_FAILED) { |
311 | activation->setHint(click::Preview::Actions::DOWNLOAD_FAILED, unity::scopes::Variant(true)); |
312 | activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview); |
313 | - |
314 | } else if (action_id == click::Preview::Actions::DOWNLOAD_COMPLETED) { |
315 | activation->setHint(click::Preview::Actions::DOWNLOAD_COMPLETED, unity::scopes::Variant(true)); |
316 | activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview); |
317 | |
318 | === modified file 'scope/tests/CMakeLists.txt' |
319 | --- scope/tests/CMakeLists.txt 2014-02-17 08:45:26 +0000 |
320 | +++ scope/tests/CMakeLists.txt 2014-02-23 22:50:47 +0000 |
321 | @@ -58,3 +58,4 @@ |
322 | |
323 | add_subdirectory(integration) |
324 | add_subdirectory(download_manager_tool) |
325 | +add_subdirectory(click_interface_tool) |
326 | \ No newline at end of file |
327 | |
328 | === added directory 'scope/tests/click_interface_tool' |
329 | === added file 'scope/tests/click_interface_tool/CMakeLists.txt' |
330 | --- scope/tests/click_interface_tool/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
331 | +++ scope/tests/click_interface_tool/CMakeLists.txt 2014-02-23 22:50:47 +0000 |
332 | @@ -0,0 +1,14 @@ |
333 | +set(CLICK_INTERFACE_TOOL_TARGET click_interface_tool) |
334 | + |
335 | +include_directories ( |
336 | + ${CMAKE_SOURCE_DIR}/scope/click |
337 | + ${CMAKE_BINARY_DIR}/scope/click |
338 | +) |
339 | + |
340 | +add_executable (${CLICK_INTERFACE_TOOL_TARGET} |
341 | + click_interface_tool.cpp |
342 | +) |
343 | + |
344 | +target_link_libraries (${CLICK_INTERFACE_TOOL_TARGET} |
345 | + ${SCOPE_LIB_UNVERSIONED} |
346 | +) |
347 | |
348 | === added file 'scope/tests/click_interface_tool/click_interface_tool.cpp' |
349 | --- scope/tests/click_interface_tool/click_interface_tool.cpp 1970-01-01 00:00:00 +0000 |
350 | +++ scope/tests/click_interface_tool/click_interface_tool.cpp 2014-02-23 22:50:47 +0000 |
351 | @@ -0,0 +1,69 @@ |
352 | +/* |
353 | + * Copyright (C) 2014 Canonical Ltd. |
354 | + * |
355 | + * This program is free software: you can redistribute it and/or modify it |
356 | + * under the terms of the GNU General Public License version 3, as published |
357 | + * by the Free Software Foundation. |
358 | + * |
359 | + * This program is distributed in the hope that it will be useful, but |
360 | + * WITHOUT ANY WARRANTY; without even the implied warranties of |
361 | + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
362 | + * PURPOSE. See the GNU General Public License for more details. |
363 | + * |
364 | + * You should have received a copy of the GNU General Public License along |
365 | + * with this program. If not, see <http://www.gnu.org/licenses/>. |
366 | + * |
367 | + * In addition, as a special exception, the copyright holders give |
368 | + * permission to link the code of portions of this program with the |
369 | + * OpenSSL library under certain conditions as described in each |
370 | + * individual source file, and distribute linked combinations |
371 | + * including the two. |
372 | + * You must obey the GNU General Public License in all respects |
373 | + * for all of the code used other than OpenSSL. If you modify |
374 | + * file(s) with this exception, you may extend this exception to your |
375 | + * version of the file(s), but you are not obligated to do so. If you |
376 | + * do not wish to do so, delete this exception statement from your |
377 | + * version. If you delete this exception statement from all source |
378 | + * files in the program, then also delete it here. |
379 | + */ |
380 | + |
381 | +#include <QCoreApplication> |
382 | +#include <QDebug> |
383 | +#include <QString> |
384 | +#include <QTimer> |
385 | +#include <QTextStream> |
386 | + |
387 | +#include <iostream> |
388 | + |
389 | +#include <interface.h> |
390 | +#include <key_file_locator.h> |
391 | + |
392 | +int main(int argc, char *argv[]) |
393 | +{ |
394 | + |
395 | + QCoreApplication a(argc, argv); |
396 | + QSharedPointer<click::KeyFileLocator> keyFileLocator(new click::KeyFileLocator()); |
397 | + click::Interface ci(keyFileLocator); |
398 | + |
399 | + |
400 | + QTimer timer; |
401 | + timer.setSingleShot(true); |
402 | + |
403 | + QObject::connect(&timer, &QTimer::timeout, [&]() { |
404 | + ci.get_dotdesktop_filename(std::string(argv[1]), |
405 | + [&a] (std::string val, click::ManifestError error){ |
406 | + if (error == click::ManifestError::NoError) { |
407 | + std::cout << " Success, got dotdesktop:" << val << std::endl; |
408 | + } else { |
409 | + std::cout << " Error:" << val << std::endl; |
410 | + } |
411 | + a.quit(); |
412 | + }); |
413 | + } ); |
414 | + |
415 | + timer.start(0); |
416 | + |
417 | + qInstallMessageHandler(0); |
418 | + return a.exec(); |
419 | +} |
420 | + |
421 | |
422 | === modified file 'scope/tests/test_index.cpp' |
423 | --- scope/tests/test_index.cpp 2014-02-23 22:50:47 +0000 |
424 | +++ scope/tests/test_index.cpp 2014-02-23 22:50:47 +0000 |
425 | @@ -283,7 +283,7 @@ |
426 | "/tmp/foo.png", |
427 | "", "0.1.5" |
428 | }; |
429 | - std::string expected = "pkcon -p remove org.example.testapp;0.1.5;all;local;click"; |
430 | + std::string expected = "pkcon -p remove org.example.testapp;0.1.5;all;local:click"; |
431 | EXPECT_CALL(*this, execute_uninstall_command(expected, _)).Times(1); |
432 | uninstall(package, [](int, std::string) {}); |
433 | } |
PASSED: Continuous integration, rev:173 jenkins. qa.ubuntu. com/job/ unity-scope- click-ci/ 345/ jenkins. qa.ubuntu. com/job/ unity-scope- click-trusty- amd64-ci/ 246 jenkins. qa.ubuntu. com/job/ unity-scope- click-trusty- armhf-ci/ 243 jenkins. qa.ubuntu. com/job/ unity-scope- click-trusty- armhf-ci/ 243/artifact/ work/output/ *zip*/output. zip
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/unity- scope-click- ci/345/ rebuild
http://