Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2024 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "extpluginsloader.h"
23 :
24 : #include "global/io/dir.h"
25 : #include "global/io/file.h"
26 : #include "global/io/fileinfo.h"
27 : #include "global/runtime.h"
28 :
29 : #include "log.h"
30 :
31 : const std::string DEV_PLUGINS("dev_test_");
32 :
33 : using namespace muse;
34 : using namespace muse::extensions;
35 : using namespace muse::extensions::legacy;
36 :
37 4 : static Uri makeUri(const io::path_t& rootPath, const io::path_t& path)
38 : {
39 4 : std::string pathStr = path.toQString().sliced(rootPath.size()).toLower().toStdString();
40 4 : std::replace(pathStr.begin(), pathStr.end(), '.', '-');
41 8 : return Uri("extension://plugin/v1" + pathStr);
42 4 : }
43 :
44 0 : std::map<std::string /*codeKey*/, Uri> ExtPluginsLoader::loadCodekeyUriMap(const io::path_t& defPath, const io::path_t& extPath) const
45 : {
46 0 : std::map<std::string /*codeKey*/, Uri> data;
47 :
48 0 : auto loadUris = [this](std::map<std::string /*codeKey*/, Uri>& data, const io::path_t& rootPath) {
49 0 : io::paths_t paths = qmlsPaths(rootPath);
50 0 : for (const io::path_t& path : paths) {
51 0 : std::string codeKey = io::completeBasename(path).toStdString();
52 0 : Uri uri = makeUri(rootPath, path);
53 0 : data.insert({ codeKey, uri });
54 0 : }
55 0 : };
56 :
57 0 : loadUris(data, defPath);
58 0 : loadUris(data, extPath);
59 :
60 : //! NOTE These plugins have been ported to extensions
61 0 : data["colornotes"] = Uri("extension://notation/colornotes");
62 0 : data["addCourtesyAccidentals"] = Uri("extension://notation/courtesy_accidentals");
63 :
64 0 : return data;
65 0 : }
66 :
67 4 : ManifestList ExtPluginsLoader::loadManifestList(const io::path_t& defPath, const io::path_t& extPath) const
68 : {
69 5 : TRACEFUNC;
70 :
71 4 : LOGD() << "try load extensions, def: " << defPath << ", user: " << extPath;
72 :
73 4 : ManifestList defaultManifests = manifestList(defPath);
74 4 : ManifestList externalManifests = manifestList(extPath);
75 :
76 4 : ManifestList retList;
77 8 : for (const Manifest& m : defaultManifests) {
78 4 : if (!m.isValid()) {
79 0 : continue;
80 : }
81 4 : retList.push_back(m);
82 : }
83 :
84 4 : for (Manifest& m : externalManifests) {
85 0 : if (!m.isValid()) {
86 0 : continue;
87 : }
88 0 : m.isRemovable = true;
89 0 : retList.push_back(m);
90 : }
91 :
92 4 : return retList;
93 4 : }
94 :
95 8 : ManifestList ExtPluginsLoader::manifestList(const io::path_t& rootPath) const
96 : {
97 8 : ManifestList manifests;
98 8 : io::paths_t paths = qmlsPaths(rootPath);
99 12 : for (const io::path_t& path : paths) {
100 4 : if (!runtime::isDebug() && (path.toStdString().find(DEV_PLUGINS) != std::string::npos)) {
101 0 : continue;
102 : }
103 :
104 4 : Manifest manifest = parseManifest(rootPath, path);
105 4 : if (!manifest.isValid()) {
106 0 : continue;
107 : }
108 4 : manifest.path = path;
109 4 : resolvePaths(manifest, io::FileInfo(path).dirPath());
110 4 : manifests.push_back(manifest);
111 4 : }
112 :
113 8 : return manifests;
114 8 : }
115 :
116 8 : io::paths_t ExtPluginsLoader::qmlsPaths(const io::path_t& rootPath) const
117 : {
118 16 : RetVal<io::paths_t> paths = io::Dir::scanFiles(rootPath, { "*.qml" });
119 8 : if (!paths.ret) {
120 0 : LOGE() << "failed scan files, err: " << paths.ret.toString()
121 0 : << ", path: " << rootPath;
122 : }
123 :
124 16 : return paths.val;
125 8 : }
126 :
127 4 : Manifest ExtPluginsLoader::parseManifest(const io::path_t& rootPath, const io::path_t& path) const
128 : {
129 4 : ByteArray data;
130 4 : Ret ret = io::File::readFile(path, data);
131 4 : if (!ret) {
132 0 : LOGE() << "failed read file: " << path << ", err: " << ret.toString();
133 0 : return Manifest();
134 : }
135 :
136 4 : String content = String::fromUtf8(data);
137 :
138 : //! NOTE Check is MuseScore plugin
139 : {
140 4 : size_t bracketPos = 0;
141 128 : for (size_t i = 0; i < content.size(); ++i) {
142 128 : if (content.at(i) == Char(u'{')) {
143 4 : bracketPos = i;
144 4 : break;
145 : }
146 : }
147 :
148 4 : if (bracketPos == 0) {
149 0 : LOGD() << "Not MuseScore plugin, path: " << path;
150 0 : return Manifest();
151 : }
152 :
153 4 : String header = content.left(bracketPos);
154 4 : if (!content.contains(u"MuseScore")) {
155 0 : LOGD() << "Not MuseScore plugin, path: " << path;
156 0 : return Manifest();
157 : }
158 4 : }
159 :
160 4 : io::FileInfo fi(path);
161 :
162 4 : Manifest m;
163 4 : m.uri = makeUri(rootPath, path);
164 4 : m.type = Type::Macros;
165 4 : m.apiversion = 1;
166 4 : m.legacyPlugin = true;
167 :
168 12 : auto dropQuotes = [](String str) {
169 12 : while (str.back() == ';') {
170 0 : str.truncate(str.size() - 1);
171 : }
172 12 : if (str.size() < 3) {
173 1 : return String();
174 : }
175 11 : if (str.startsWith(u"qsTr(\"") && str.endsWith(u"\")")) {
176 0 : return str.mid(6, str.size() - 8);
177 : }
178 :
179 11 : if (str.startsWith(u"\"") && str.endsWith(u"\"")) {
180 7 : return str.mid(1, str.size() - 2);
181 : }
182 :
183 4 : return str;
184 : };
185 :
186 4 : m.context = std::string(DEFAULT_CONTEXT);
187 4 : int needProperties = 7; // title, description, pluginType, category, thumbnail, requiresScore, version
188 4 : int propertiesFound = 0;
189 4 : bool docked = false;
190 4 : bool insideMuseScoreItem = false;
191 4 : size_t current, previous = 0;
192 4 : current = content.indexOf(u"\n");
193 :
194 : //! NOTE Supposed structure
195 : //! // comments
196 : //! import ...
197 : //! import ...
198 : //!
199 : //! MuseScore {
200 : //!
201 : //! prop1: ...
202 : //! prop2: ...
203 : //! ...
204 : //!
205 : //! As soon as the first open bracket is encountered,
206 : //! we consider that the properties have ended.
207 : //! Technically, there may be properties further down,
208 : //! we just need to ask to move them up.
209 : //!
210 : //! onRun: {..
211 : //! function applyMirrorIntervals() {..
212 : //! Item { ...
213 : //!
214 :
215 28 : while (current != std::string::npos) {
216 24 : String line = content.mid(previous, current - previous).trimmed();
217 :
218 : //! NOTE Needed for compatibility with 3.6
219 : //! We can add properties from 4.4 that are not in 3.6
220 24 : if (line.startsWith(u"//4.4 ")) {
221 0 : line = line.mid(6);
222 : }
223 :
224 24 : if (line.startsWith(u'/')) { // comment
225 : // noop
226 24 : } else if (line.endsWith(u'{')) {
227 4 : if (insideMuseScoreItem) {
228 0 : break;
229 : }
230 :
231 4 : if (!insideMuseScoreItem) {
232 4 : insideMuseScoreItem = true;
233 : }
234 20 : } else if (line.startsWith(u"title:")) {
235 4 : m.title = dropQuotes(line.mid(6).trimmed());
236 4 : ++propertiesFound;
237 16 : } else if (line.startsWith(u"description:")) {
238 0 : m.description = dropQuotes(line.mid(12).trimmed());
239 0 : ++propertiesFound;
240 16 : } else if (line.startsWith(u"pluginType:")) {
241 4 : String pluginType = dropQuotes(line.mid(11).trimmed());
242 4 : docked = pluginType == "dock";
243 4 : if (pluginType == "dialog" || pluginType == "dock") {
244 2 : m.type = Type::Form;
245 : }
246 4 : ++propertiesFound;
247 16 : } else if (line.startsWith(u"categoryCode:")) {
248 0 : m.category = dropQuotes(line.mid(13).trimmed());
249 0 : ++propertiesFound;
250 12 : } else if (line.startsWith(u"thumbnailName:")) {
251 0 : m.thumbnail = dropQuotes(line.mid(14).trimmed()).toStdString();
252 0 : ++propertiesFound;
253 12 : } else if (line.startsWith(u"requiresScore:")) {
254 4 : String requiresScore = dropQuotes(line.mid(14).trimmed());
255 4 : if (requiresScore == u"false") {
256 4 : m.context = "Any";
257 : }
258 4 : ++propertiesFound;
259 12 : } else if (line.startsWith(u"version:")) {
260 0 : m.version = dropQuotes(line.mid(8).trimmed()).toStdString();
261 0 : ++propertiesFound;
262 : }
263 :
264 24 : if (propertiesFound == needProperties) {
265 0 : break;
266 : }
267 :
268 24 : previous = current + 1;
269 24 : current = content.indexOf(u"\n", previous);
270 24 : }
271 :
272 4 : if (m.title.isEmpty()) {
273 0 : m.title = fi.baseName();
274 : }
275 :
276 4 : Action a;
277 4 : if (docked) {
278 2 : m.context = std::string(PROJECT_OPENED_CONTEXT);
279 : }
280 4 : a.code = "main";
281 4 : a.type = m.type;
282 4 : a.docked = docked;
283 4 : a.title = m.title;
284 4 : a.context = m.context;
285 4 : a.apiversion = m.apiversion;
286 4 : a.legacyPlugin = m.legacyPlugin;
287 4 : a.path = fi.fileName();
288 4 : m.actions.push_back(std::move(a));
289 :
290 4 : return m;
291 4 : }
292 :
293 4 : void ExtPluginsLoader::resolvePaths(Manifest& m, const io::path_t& rootDirPath) const
294 : {
295 4 : if (!m.thumbnail.empty()) {
296 0 : m.thumbnail = rootDirPath + "/" + m.thumbnail;
297 : }
298 :
299 8 : for (Action& a : m.actions) {
300 4 : a.path = rootDirPath + "/" + a.path;
301 : }
302 4 : }
|