Plugins
Authors: Francois Tadel
Brainstorm connects with features from many third-party libraries of methods. The external software can be downloaded or updated automatically by Brainstorm when needed. This tutorial presents the API to register and manage plugins.
Interactive management
The Brainstorm interface offers menus to Install/Update/Uninstall plugins.
Install: The package is downloaded in the Brainstorm user folder: $HOME/.brainstorm/plugins/
Uninstall: Delete the plugin folder and all its subfolders.
Load: Adds all the subfolders needed by the plugin to the Matlab path, plus other optional taks.
Unload: Removes all the plugin folders from the Matlab path.
Update: Some plugins are designed to update themselves automatically whenever a new version is available online, or requested by Brainstorm. Others plugins must be updated manually.
Manual install: If you already have a given plugin installed on your computer (eg. FieldTrip, SPM12) and don't want Brainstorm to manage the download/update or the Matlab path for you, reference it with the menu: Custom install > Set installation folder.
List: You can list all the installed plugins with the menu List:
Command-line management
The calls to install or manage plugins are all documented in the header of bst_plugin.m:
1 function [varargout] = bst_plugin(varargin)
2 % BST_PLUGIN: Manages Brainstorm plugins
3 %
4 % USAGE: PlugDesc = bst_plugin('GetSupported') % List all the plugins supported by Brainstorm
5 % PlugDesc = bst_plugin('GetSupported', PlugName/PlugDesc) % Get only one specific supported plugin
6 % PlugDesc = bst_plugin('GetInstalled') % Get all the installed plugins
7 % PlugDesc = bst_plugin('GetInstalled', PlugName/PlugDesc) % Get a specific installed plugin
8 % PlugDesc = bst_plugin('GetLoaded') % Get all the loaded plugins
9 % [PlugDesc, errMsg] = bst_plugin('GetDescription', PlugName/PlugDesc) % Get a full structure representing a plugin
10 % [Version, URLzip] = bst_plugin('GetVersionOnline', PlugName, URLzip, isCache) % Get the latest online version of some plugins
11 % sha = bst_plugin('GetGithubCommit', URLzip) % Get SHA of the last commit of a GitHub repository from a master.zip url
12 % ReadmeFile = bst_plugin('GetReadmeFile', PlugDesc) % Get full path to plugin readme file
13 % LogoFile = bst_plugin('GetLogoFile', PlugDesc) % Get full path to plugin logo file
14 % Version = bst_plugin('CompareVersions', v1, v2) % Compare two version strings
15 % [isOk, errMsg] = bst_plugin('AddUserDefDesc', RegMethod, jsonLocation=[]) % Register user-defined plugin definition
16 % [isOk, errMsg] = bst_plugin('RemoveUserDefDesc' PlugName) % Remove user-defined plugin definition
17 % [isOk, errMsg, PlugDesc] = bst_plugin('Load', PlugName/PlugDesc, isVerbose=1)
18 % [isOk, errMsg, PlugDesc] = bst_plugin('LoadInteractive', PlugName/PlugDesc)
19 % [isOk, errMsg, PlugDesc] = bst_plugin('Unload', PlugName/PlugDesc, isVerbose=1)
20 % [isOk, errMsg, PlugDesc] = bst_plugin('UnloadInteractive', PlugName/PlugDesc)
21 % [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[]) % Install and Load a plugin and its dependencies
22 % [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice',PlugNames, isInteractive=0) % Install at least one of the input plugins
23 % [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
24 % [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
25 % [isOk, errMsg] = bst_plugin('UninstallInteractive', PlugName)
26 % [eRes errMsg, PlugDesc] = bst_plugin('Ensure', PlugName, isInteractive=0, getLatestVersion=0) % Ensure that the plugin is available, eRes = 0 Already installed and loaded, eRes = 1 Install/Load performed, eRes = 2 Load performed
27 % bst_plugin('Configure', PlugDesc) % Execute some additional tasks after loading or installation
28 % bst_plugin('SetCustomPath', PlugName, PlugPath)
29 % bst_plugin('List', Target='installed') % Target={'supported','installed'}
30 % bst_plugin('Archive', OutputFile=[ask]) % Archive software environment
31 % bst_plugin('MenuCreate', jMenu)
32 % bst_plugin('MenuUpdate', jMenu)
33 % bst_plugin('LinkSpmToolbox', Action, Toolbox) % 0=Delete/1=Create/2=Check a symbolic link for a Toolbox in SPM12 toolbox folder
34 % bst_plugin('UpdateDescription', PlugDesc, doDelete=0) % Update plugin description after load
35 %
36 %
37 % PLUGIN DEFINITION
38 % =================
39 %
40 % The plugins registered in Brainstorm are listed in function GetSupported().
41 % Each one is an entry in the PlugDesc array, following the structure defined in db_template('plugdesc').
42 % The fields allowed are described below.
43 %
44 % Mandatory fields
45 % ================
46 % - Name : String: Plugin name = subfolder in the Brainstorm user folder
47 % - Version : String: Version of the plugin (eg. '1.2', '21a', 'github-master', 'latest')
48 % - URLzip : String: Download URL, zip or tgz file accessible over HTTP/HTTPS/FTP
49 % - URLinfo : String: Information URL = Software website
50 %
51 % Optional fields
52 % ===============
53 % - AutoUpdate : Boolean: If true, the plugin is updated automatically when there is a new version available (default: true).
54 % - AutoLoad : Boolean: If true, the plugin is loaded automatically at Brainstorm startup
55 % - Category : String: Sub-menu in which the plugin is listed
56 % - ExtraMenus : Cell matrix {Nx2}: List of entries to add to the plugins menu
57 % | ExtraMenus{i,1}: String: Label of the menu
58 % | ExtraMenus{i,2}: String: Matlab code to eval when the menu is clicked
59 % - TestFile : String: Name of a file that should be located in one of the loaded folders of the plugin (eg. 'spm.m' for SPM12).
60 % | This is used to test whether the plugin was correctly installed, or whether it is available somewhere else in the Matlab path.
61 % - ReadmeFile : String: Name of the text file to display after installing the plugin (must be in the plugin folder).
62 % | If empty, it tries using brainstorm3/doc/plugin/plugname_readme.txt
63 % - LogoFile : String: Name of the image file to display during the plugin download, installation, and associated computations (must be in the plugin folder).
64 % | Supported extensions: gif, png. If empty, try using brainstorm3/doc/plugin/<Name>_logo.[gif|png]
65 % - MinMatlabVer : Integer: Minimum Matlab version required for using this plugin, as returned by bst_get('MatlabVersion')
66 % - CompiledStatus : Integer: Behavior of this plugin in the compiled version of Brainstorm:
67 % | 0: Plugin is not available in the compiled distribution of Brainstorm
68 % | 1: Plugin is available for download (only for plugins based on native compiled code)
69 % | 2: Plugin is included in the compiled distribution of Brainstorm
70 % - RequiredPlugs : Cell-array: Additional plugins required by this plugin, that must be installed/loaded beforehand.
71 % | {Nx2} => {'plugname','version'; ...} or
72 % | {Nx1} => {'plugname'; ...}
73 % - UnloadPlugs : Cell-array of names of incompatible plugin, to unload before loaing this one
74 % - LoadFolders : Cell-array of subfolders to add to the Matlab path when setting up the plugin. Use {'*'} to add all the plugin subfolders.
75 % - GetVersionFcn : String to eval or function handle to call to get the version after installation
76 % - InstalledFcn : String to eval or function handle to call after installing the plugin
77 % - UninstalledFcn : String to eval or function handle to call after uninstalling the plugin
78 % - LoadedFcn : String to eval or function handle to call after loading the plugin
79 % - UnloadedFcn : String to eval or function handle to call after unloading the plugin
80 % - DeleteFiles : List of files to delete after installation
81 %
82 % Fields set when installing the plugin
83 % =====================================
84 % - Processes : List of process functions to be added to the pipeline manager
85 %
86 % Fields set when loading the plugin
87 % ==================================
88 % - Path : Installation path (eg. /home/username/.brainstorm/plugins/fieldtrip)
89 % - SubFolder : If all the code is in a single subfolder (eg. /plugins/fieldtrip/fieldtrip-20210304),
90 % this is detected and the full path to the TestFile would be typically fullfile(Path, SubFolder).
91 % - isLoaded : 0=Not loaded, 1=Loaded
92 % - isManaged : 0=Installed manually by the user, 1=Installed automatically by Brainstorm
93 %
94
95 % @=============================================================================
96 % This function is part of the Brainstorm software:
97 % https://neuroimage.usc.edu/brainstorm
98 %
99 % Copyright (c) University of Southern California & McGill University
100 % This software is distributed under the terms of the GNU General Public License
101 % as published by the Free Software Foundation. Further details on the GPLv3
102 % license can be found at http://www.gnu.org/copyleft/gpl.html.
103 %
104 % FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE
105 % UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY
106 % WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
107 % MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY
108 % LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE.
109 %
110 % For more information type "brainstorm license" at command prompt.
111 % =============================================================================@
112 %
113 % Authors: Francois Tadel, 2021-2023
114
115 eval(macro_method);
116 end
117
118
119 %% ===== GET SUPPORTED PLUGINS =====
120 % USAGE: PlugDesc = bst_plugin('GetSupported') % List all the plugins supported by Brainstorm
121 % PlugDesc = bst_plugin('GetSupported', PlugName/PlugDesc) % Get only one specific supported plugin
122 % PlugDesc = bst_plugin('GetSupported', ..., UserDefVerbose) % Print info on user-defined plugins
123 function PlugDesc = GetSupported(SelPlug, UserDefVerbose)
124 % Parse inputs
125 if (nargin < 2) || isempty(UserDefVerbose)
126 UserDefVerbose = 0;
127 end
128 if (nargin < 1) || isempty(SelPlug)
129 SelPlug = [];
130 end
131 % Initialized returned structure
132 PlugDesc = repmat(db_template('PlugDesc'), 0);
133 % Get OS
134 OsType = bst_get('OsType', 0);
135
136 % Add new curated plugins by 'CATEGORY:' and alphabetic order
137 % ================================================================================================================
138 % === ANATOMY: BRAIN2MESH ===
139 PlugDesc(end+1) = GetStruct('brain2mesh');
140 PlugDesc(end).Version = 'github-master';
141 PlugDesc(end).Category = 'Anatomy';
142 PlugDesc(end).URLzip = 'https://github.com/fangq/brain2mesh/archive/master.zip';
143 PlugDesc(end).URLinfo = 'https://mcx.space/brain2mesh/';
144 PlugDesc(end).TestFile = 'brain2mesh.m';
145 PlugDesc(end).ReadmeFile = 'README.md';
146 PlugDesc(end).CompiledStatus = 2;
147 PlugDesc(end).RequiredPlugs = {'spm12'; 'iso2mesh'};
148 PlugDesc(end).DeleteFiles = {'examples', 'brain1020.m', 'closestnode.m', 'label2tpm.m', 'slicesurf.m', 'slicesurf3.m', 'tpm2label.m', 'polylineinterp.m', 'polylinelen.m', 'polylinesimplify.m'};
149
150 % === ANATOMY: CAT12 ===
151 PlugDesc(end+1) = GetStruct('cat12');
152 PlugDesc(end).Version = 'latest';
153 PlugDesc(end).Category = 'Anatomy';
154 PlugDesc(end).AutoUpdate = 1;
155 PlugDesc(end).URLzip = 'https://www.neuro.uni-jena.de/cat12/cat12_latest.zip';
156 PlugDesc(end).URLinfo = 'https://www.neuro.uni-jena.de/cat/';
157 PlugDesc(end).TestFile = 'cat_version.m';
158 PlugDesc(end).ReadmeFile = 'Contents.txt';
159 PlugDesc(end).CompiledStatus = 0;
160 PlugDesc(end).RequiredPlugs = {'spm12'};
161 PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @cat_version)';
162 PlugDesc(end).InstalledFcn = 'LinkSpmToolbox(1, ''cat12'');';
163 PlugDesc(end).UninstalledFcn = 'LinkSpmToolbox(0, ''cat12'');';
164 PlugDesc(end).LoadedFcn = 'LinkSpmToolbox(2, ''cat12'');';
165 PlugDesc(end).UnloadedFcn = 'LinkSpmToolbox(0, ''cat12'');';
166 PlugDesc(end).ExtraMenus = {'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/SegCAT12'', ''-browser'')'};
167
168 % === ANATOMY: CT2MRIREG ===
169 PlugDesc(end+1) = GetStruct('ct2mrireg');
170 PlugDesc(end).Version = 'github-master';
171 PlugDesc(end).Category = 'Anatomy';
172 PlugDesc(end).AutoUpdate = 1;
173 PlugDesc(end).URLzip = 'https://github.com/ajoshiusc/USCCleveland/archive/master.zip';
174 PlugDesc(end).URLinfo = 'https://github.com/ajoshiusc/USCCleveland/tree/master/ct2mrireg';
175 PlugDesc(end).TestFile = 'ct2mrireg.m';
176 PlugDesc(end).ReadmeFile = 'ct2mrireg/README.md';
177 PlugDesc(end).CompiledStatus = 2;
178 PlugDesc(end).LoadFolders = {'ct2mrireg'};
179 PlugDesc(end).DeleteFiles = {'fmri_analysis', 'for_clio', 'mixed_atlas', 'process_script', 'reg_prepost', 'visualize_channels', '.gitignore', 'README.md'};
180
181 % === ANATOMY: ISO2MESH ===
182 PlugDesc(end+1) = GetStruct('iso2mesh');
183 PlugDesc(end).Version = 'github-master';
184 PlugDesc(end).Category = 'Anatomy';
185 PlugDesc(end).AutoUpdate = 1;
186 PlugDesc(end).URLzip = 'https://github.com/fangq/iso2mesh/archive/master.zip';
187 PlugDesc(end).URLinfo = 'https://iso2mesh.sourceforge.net';
188 PlugDesc(end).TestFile = 'iso2meshver.m';
189 PlugDesc(end).ReadmeFile = 'README.txt';
190 PlugDesc(end).CompiledStatus = 2;
191 PlugDesc(end).LoadedFcn = 'assignin(''base'', ''ISO2MESH_TEMP'', bst_get(''BrainstormTmpDir''));';
192 PlugDesc(end).UnloadPlugs = {'easyh5','jsnirfy'};
193
194 % === ANATOMY: NEUROMAPS ===
195 PlugDesc(end+1) = GetStruct('neuromaps');
196 PlugDesc(end).Version = 'github-main';
197 PlugDesc(end).Category = 'Anatomy';
198 PlugDesc(end).AutoUpdate = 0;
199 PlugDesc(end).AutoLoad = 0;
200 PlugDesc(end).CompiledStatus = 2;
201 PlugDesc(end).URLzip = 'https://github.com/thuy-n/bst-neuromaps/archive/refs/heads/main.zip';
202 PlugDesc(end).URLinfo = 'https://github.com/thuy-n/bst-neuromaps';
203 PlugDesc(end).ReadmeFile = 'README.md';
204 PlugDesc(end).LoadFolders = {'*'};
205 PlugDesc(end).TestFile = 'process_nmp_fetch_maps.m';
206
207 % === ANATOMY: RESECTION IDENTIFICATION ===
208 PlugDesc(end+1) = GetStruct('resection-identification');
209 PlugDesc(end).Version = 'latest';
210 PlugDesc(end).Category = 'Anatomy';
211 PlugDesc(end).AutoUpdate = 1;
212 PlugDesc(end).URLzip = ['https://neuroimage.usc.edu/bst/getupdate.php?d=bst_resection_identification_' OsType '.zip'];
213 PlugDesc(end).TestFile = 'resection_identification';
214 if strcmp(OsType, 'win64')
215 PlugDesc(end).TestFile = [PlugDesc(end).TestFile, '.bat'];
216 end
217 PlugDesc(end).URLinfo = 'https://github.com/ajoshiusc/auto_resection_mask/tree/brainstorm-plugin';
218 PlugDesc(end).CompiledStatus = 1;
219 PlugDesc(end).LoadFolders = {'bin'};
220
221 % === ANATOMY: ROAST ===
222 PlugDesc(end+1) = GetStruct('roast');
223 PlugDesc(end).Version = '3.0';
224 PlugDesc(end).Category = 'Anatomy';
225 PlugDesc(end).AutoUpdate = 1;
226 PlugDesc(end).URLzip = 'https://www.parralab.org/roast/roast-3.0.zip';
227 PlugDesc(end).URLinfo = 'https://www.parralab.org/roast/';
228 PlugDesc(end).TestFile = 'roast.m';
229 PlugDesc(end).ReadmeFile = 'README.md';
230 PlugDesc(end).CompiledStatus = 0;
231 PlugDesc(end).UnloadPlugs = {'spm12', 'iso2mesh'};
232 PlugDesc(end).LoadFolders = {'lib/spm12', 'lib/iso2mesh', 'lib/cvx', 'lib/ncs2daprox', 'lib/NIFTI_20110921'};
233
234 % === ANATOMY: ZEFFIRO ===
235 PlugDesc(end+1) = GetStruct('zeffiro');
236 PlugDesc(end).Version = 'github-main_development_branch';
237 PlugDesc(end).Category = 'Anatomy';
238 PlugDesc(end).AutoUpdate = 1;
239 PlugDesc(end).URLzip = 'https://github.com/sampsapursiainen/zeffiro_interface/archive/main_development_branch.zip';
240 PlugDesc(end).URLinfo = 'https://github.com/sampsapursiainen/zeffiro_interface';
241 PlugDesc(end).TestFile = 'zeffiro_downloader.m';
242 PlugDesc(end).ReadmeFile = 'README.md';
243 PlugDesc(end).CompiledStatus = 0;
244 PlugDesc(end).LoadFolders = {'*'};
245 PlugDesc(end).DeleteFiles = {'.gitignore'};
246
247
248 % === ARTIFACTS: GEDAI ===
249 PlugDesc(end+1) = GetStruct('gedai');
250 PlugDesc(end).Version = '3b613b8c';
251 PlugDesc(end).Category = 'Artifacts';
252 PlugDesc(end).URLzip = 'https://github.com/neurotuning/GEDAI-master/archive/3b613b8cf3e6736b6a3b7e1fe35b6066afc312cf.zip';
253 PlugDesc(end).URLinfo = 'https://github.com/neurotuning/GEDAI-master';
254 PlugDesc(end).TestFile = 'process_gedai.m';
255 PlugDesc(end).ReadmeFile = 'README.md';
256 PlugDesc(end).AutoLoad = 0;
257 PlugDesc(end).CompiledStatus = 2;
258 PlugDesc(end).LoadFolders = {'*'};
259 PlugDesc(end).DeleteFiles = {'.git', 'example data'};
260
261
262 % === FORWARD: OPENMEEG ===
263 PlugDesc(end+1) = GetStruct('openmeeg');
264 PlugDesc(end).Version = '2.4.1';
265 PlugDesc(end).Category = 'Forward';
266 PlugDesc(end).AutoUpdate = 1;
267 switch(OsType)
268 case 'linux64'
269 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-Linux.tar.gz';
270 PlugDesc(end).TestFile = 'libOpenMEEG.so';
271 case 'mac64'
272 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-MacOSX.tar.gz';
273 PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
274 case 'mac64arm'
275 PlugDesc(end).Version = '2.5.8';
276 PlugDesc(end).URLzip = ['https://github.com/openmeeg/openmeeg/releases/download/', PlugDesc(end).Version, '/OpenMEEG-', PlugDesc(end).Version, '-', 'macOS_M1.tar.gz'];
277 PlugDesc(end).TestFile = 'libOpenMEEG.1.1.0.dylib';
278 case 'win32'
279 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/release-2.2/OpenMEEG-2.2.0-win32-x86-cl-OpenMP-shared.tar.gz';
280 PlugDesc(end).TestFile = 'om_assemble.exe';
281 case 'win64'
282 PlugDesc(end).URLzip = 'https://files.inria.fr/OpenMEEG/download/OpenMEEG-2.4.1-Win64.tar.gz';
283 PlugDesc(end).TestFile = 'om_assemble.exe';
284 end
285 PlugDesc(end).URLinfo = 'https://openmeeg.github.io/';
286 PlugDesc(end).ExtraMenus = {'Alternate versions', 'web(''https://files.inria.fr/OpenMEEG/download/'', ''-browser'')'; ...
287 'Download Visual C++', 'web(''https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170'', ''-browser'')'; ...
288 'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/TutBem'', ''-browser'')'};
289 PlugDesc(end).CompiledStatus = 1;
290 PlugDesc(end).LoadFolders = {'bin', 'lib'};
291
292 % === FORWARD: DUNEURO ===
293 PlugDesc(end+1) = GetStruct('duneuro');
294 PlugDesc(end).Version = 'latest';
295 PlugDesc(end).Category = 'Forward';
296 PlugDesc(end).AutoUpdate = 1;
297 PlugDesc(end).URLzip = 'https://neuroimage.usc.edu/bst/getupdate.php?d=bst_duneuro.zip';
298 PlugDesc(end).URLinfo = 'https://neuroimage.usc.edu/brainstorm/Tutorials/Duneuro';
299 PlugDesc(end).TestFile = 'bst_duneuro_meeg_win64.exe';
300 PlugDesc(end).CompiledStatus = 1;
301 PlugDesc(end).LoadFolders = {'bin'};
302
303 % === INVERSE: BRAINENTROPY ===
304 PlugDesc(end+1) = GetStruct('brainentropy');
305 PlugDesc(end).Version = 'github-master';
306 PlugDesc(end).Category = 'Inverse';
307 PlugDesc(end).AutoUpdate = 1;
308 PlugDesc(end).URLzip = 'https://github.com/multi-funkim/best-brainstorm/archive/master.zip';
309 PlugDesc(end).URLinfo = 'https://neuroimage.usc.edu/brainstorm/Tutorials/TutBEst';
310 PlugDesc(end).TestFile = 'process_inverse_mem.m';
311 PlugDesc(end).AutoLoad = 1;
312 PlugDesc(end).CompiledStatus = 2;
313 PlugDesc(end).LoadFolders = {'*'};
314 PlugDesc(end).GetVersionFcn = @be_versions;
315 PlugDesc(end).DeleteFiles = {'docs', '.github'};
316
317
318 % === I/O: ADI-SDK === ADInstrument SDK for reading LabChart files
319 PlugDesc(end+1) = GetStruct('adi-sdk');
320 PlugDesc(end).Version = 'github-master';
321 PlugDesc(end).Category = 'I/O';
322 switch (OsType)
323 case 'win64', PlugDesc(end).URLzip = 'https://github.com/JimHokanson/adinstruments_sdk_matlab/archive/master.zip';
324 end
325 PlugDesc(end).URLinfo = 'https://github.com/JimHokanson/adinstruments_sdk_matlab';
326 PlugDesc(end).TestFile = 'adi.m';
327 PlugDesc(end).CompiledStatus = 0;
328
329 % === I/O: AXION ===
330 PlugDesc(end+1) = GetStruct('axion');
331 PlugDesc(end).Version = '1.0';
332 PlugDesc(end).Category = 'I/O';
333 PlugDesc(end).URLzip = 'https://neuroimage.usc.edu/bst/getupdate.php?d=AxionBioSystems.zip';
334 PlugDesc(end).URLinfo = 'https://www.axionbiosystems.com/products/software/neural-module';
335 PlugDesc(end).TestFile = 'AxisFile.m';
336 % PlugDesc(end).ReadmeFile = 'README.md';
337 PlugDesc(end).CompiledStatus = 0;
338
339 % === I/O: BCI2000 ===
340 PlugDesc(end+1) = GetStruct('bci2000');
341 PlugDesc(end).Version = 'latest';
342 PlugDesc(end).Category = 'I/O';
343 PlugDesc(end).URLzip = 'https://bci2000.org/downloads/mex.zip';
344 PlugDesc(end).URLinfo = 'https://www.bci2000.org/mediawiki/index.php/User_Reference:Matlab_MEX_Files';
345 PlugDesc(end).TestFile = 'load_bcidat.m';
346 PlugDesc(end).CompiledStatus = 0;
347
348 % === I/O: BLACKROCK ===
349 PlugDesc(end+1) = GetStruct('blackrock');
350 PlugDesc(end).Version = 'github-master';
351 PlugDesc(end).Category = 'I/O';
352 PlugDesc(end).URLzip = 'https://github.com/BlackrockMicrosystems/NPMK/archive/master.zip';
353 PlugDesc(end).URLinfo = 'https://github.com/BlackrockMicrosystems/NPMK/blob/master/NPMK/Users%20Guide.pdf';
354 PlugDesc(end).TestFile = 'openNSx.m';
355 PlugDesc(end).CompiledStatus = 2;
356 PlugDesc(end).LoadFolders = {'*'};
357 PlugDesc(end).DeleteFiles = {'NPMK/installNPMK.m', 'NPMK/Users Guide.pdf', 'NPMK/Versions.txt', ...
358 'NPMK/@KTUEAImpedanceFile', 'NPMK/@KTNSPOnline', 'NPMK/@KTNEVComments', 'NPMK/@KTFigureAxis', 'NPMK/@KTFigure', 'NPMK/@KTUEAMapFile/.svn', ...
359 'NPMK/openNSxSync.m', 'NPMK/NTrode Utilities', 'NPMK/NSx Utilities', 'NPMK/NEV Utilities', 'NPMK/LoadingEngines', ...
360 'NPMK/Other tools/.svn', 'NPMK/Other tools/edgeDetect.m', 'NPMK/Other tools/kshuffle.m', 'NPMK/Other tools/openCCF.m', 'NPMK/Other tools/parseCCF.m', ...
361 'NPMK/Other tools/periEventPlot.asv', 'NPMK/Other tools/periEventPlot.m', 'NPMK/Other tools/playSound.m', ...
362 'NPMK/Dependent Functions/.svn', 'NPMK/Dependent Functions/.DS_Store', 'NPMK/Dependent Functions/bnsx.dat', 'NPMK/Dependent Functions/syncPatternDetectNEV.m', ...
363 'NPMK/Dependent Functions/syncPatternDetectNSx.m', 'NPMK/Dependent Functions/syncPatternFinderNSx.m'};
364
365 % === I/O: EASYH5 ===
366 PlugDesc(end+1) = GetStruct('easyh5');
367 PlugDesc(end).Version = 'github-master';
368 PlugDesc(end).Category = 'I/O';
369 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/easyh5/archive/master.zip';
370 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/easyh5';
371 PlugDesc(end).TestFile = 'loadh5.m';
372 PlugDesc(end).CompiledStatus = 2;
373 PlugDesc(end).LoadFolders = {'*'};
374 PlugDesc(end).DeleteFiles = {'examples'};
375 PlugDesc(end).ReadmeFile = 'README.md';
376 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
377
378 % === I/O: JNIfTI ===
379 PlugDesc(end+1) = GetStruct('jnifti');
380 PlugDesc(end).Version = '0.8';
381 PlugDesc(end).Category = 'I/O';
382 PlugDesc(end).AutoUpdate = 1;
383 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jnifty/archive/refs/tags/v0.8.zip';
384 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/jnifty';
385 PlugDesc(end).TestFile = 'jnii2nii.m';
386 PlugDesc(end).ReadmeFile = 'README.txt';
387 PlugDesc(end).CompiledStatus = 2;
388 PlugDesc(end).LoadedFcn = '';
389 PlugDesc(end).RequiredPlugs = {'jsonlab'};
390
391 % === I/O: JSNIRFY ===
392 PlugDesc(end+1) = GetStruct('jsnirfy');
393 PlugDesc(end).Version = 'github-master';
394 PlugDesc(end).Category = 'I/O';
395 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jsnirfy/archive/master.zip';
396 PlugDesc(end).URLinfo = 'https://github.com/NeuroJSON/jsnirfy';
397 PlugDesc(end).TestFile = 'loadsnirf.m';
398 PlugDesc(end).CompiledStatus = 2;
399 PlugDesc(end).LoadFolders = {'*'};
400 PlugDesc(end).DeleteFiles = {'external', '.gitmodules'};
401 PlugDesc(end).ReadmeFile = 'README.md';
402 PlugDesc(end).RequiredPlugs = {'easyh5'; 'jsonlab'};
403 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
404
405 % === I/O: JSONLab ===
406 PlugDesc(end+1) = GetStruct('jsonlab');
407 PlugDesc(end).Version = 'github-master';
408 PlugDesc(end).Category = 'I/O';
409 PlugDesc(end).URLzip = 'https://github.com/NeuroJSON/jsonlab/archive/refs/heads/master.zip';
410 PlugDesc(end).URLinfo = 'https://neurojson.org/jsonlab';
411 PlugDesc(end).TestFile = 'savejson.m';
412 PlugDesc(end).CompiledStatus = 2;
413 PlugDesc(end).LoadFolders = {'*'};
414 PlugDesc(end).DeleteFiles = {'examples', 'images', 'test', '.github', '.gitignore'};
415 PlugDesc(end).ReadmeFile = 'README.rst';
416 PlugDesc(end).UnloadPlugs = {'iso2mesh'};
417
418 % === I/O: MFF ===
419 PlugDesc(end+1) = GetStruct('mff');
420 PlugDesc(end).Version = 'github-master';
421 PlugDesc(end).Category = 'I/O';
422 PlugDesc(end).AutoUpdate = 0;
423 PlugDesc(end).URLzip = 'https://github.com/arnodelorme/mffmatlabio/archive/master.zip';
424 PlugDesc(end).URLinfo = 'https://github.com/arnodelorme/mffmatlabio';
425 PlugDesc(end).TestFile = 'eegplugin_mffmatlabio.m';
426 PlugDesc(end).ReadmeFile = 'README.md';
427 PlugDesc(end).MinMatlabVer = 803; % 2014a
428 PlugDesc(end).CompiledStatus = 0;
429 PlugDesc(end).LoadedFcn = @Configure;
430 % Stable version: https://neuroimage.usc.edu/bst/getupdate.php?d='mffmatlabio-3.5.zip'
431
432 % === I/O: NEUROELECTRICS ===
433 PlugDesc(end+1) = GetStruct('neuroelectrics');
434 PlugDesc(end).Version = '1.8';
435 PlugDesc(end).Category = 'I/O';
436 PlugDesc(end).AutoUpdate = 0;
437 PlugDesc(end).URLzip = 'https://sccn.ucsd.edu/eeglab/plugins/Neuroelectrics1.8.zip';
438 PlugDesc(end).URLinfo = 'https://www.neuroelectrics.com/wiki/index.php/EEGLAB';
439 PlugDesc(end).TestFile = 'pop_nedf.m';
440 PlugDesc(end).ReadmeFile = 'README.txt';
441 PlugDesc(end).CompiledStatus = 2;
442 PlugDesc(end).InstalledFcn = ['d=pwd; cd(fileparts(which(''pop_nedf''))); mkdir(''private''); ' ...
443 'f=fopen(''private' filesep 'eeg_emptyset.m'',''wt''); fprintf(f,''function EEG=eeg_emptyset()\nEEG=struct();''); fclose(f);' ...
444 'f=fopen(''private' filesep 'eeg_checkset.m'',''wt''); fprintf(f,''function EEG=eeg_checkset(EEG)''); fclose(f);' ...
445 'cd(d);'];
446
447 % === I/O: npy-matlab ===
448 PlugDesc(end+1) = GetStruct('npy-matlab');
449 PlugDesc(end).Version = 'github-master';
450 PlugDesc(end).Category = 'I/O';
451 PlugDesc(end).URLzip = 'https://github.com/kwikteam/npy-matlab/archive/refs/heads/master.zip';
452 PlugDesc(end).URLinfo = 'https://github.com/kwikteam/npy-matlab';
453 PlugDesc(end).TestFile = 'constructNPYheader.m';
454 PlugDesc(end).LoadFolders = {'*'};
455 PlugDesc(end).ReadmeFile = 'README.md';
456 PlugDesc(end).CompiledStatus = 0;
457
458 % === I/O: NWB ===
459 PlugDesc(end+1) = GetStruct('nwb');
460 PlugDesc(end).Version = 'github-master';
461 PlugDesc(end).Category = 'I/O';
462 PlugDesc(end).URLzip = 'https://github.com/NeurodataWithoutBorders/matnwb/archive/master.zip';
463 PlugDesc(end).URLinfo = 'https://github.com/NeurodataWithoutBorders/matnwb';
464 PlugDesc(end).TestFile = 'nwbRead.m';
465 PlugDesc(end).ReadmeFile = 'README.md';
466 PlugDesc(end).MinMatlabVer = 901; % 2016b
467 PlugDesc(end).CompiledStatus = 0;
468 PlugDesc(end).LoadFolders = {'*'};
469 PlugDesc(end).LoadedFcn = @Configure;
470
471 % === I/O: PLEXON ===
472 PlugDesc(end+1) = GetStruct('plexon');
473 PlugDesc(end).Version = '1.8.4';
474 PlugDesc(end).Category = 'I/O';
475 PlugDesc(end).URLzip = 'https://plexon.com/wp-content/uploads/2017/08/OmniPlex-and-MAP-Offline-SDK-Bundle_0.zip';
476 PlugDesc(end).URLinfo = 'https://plexon.com/software-downloads/#software-downloads-SDKs';
477 PlugDesc(end).TestFile = 'plx_info.m';
478 PlugDesc(end).ReadmeFile = 'Change Log.txt';
479 PlugDesc(end).CompiledStatus = 0;
480 PlugDesc(end).DownloadedFcn = ['d = fullfile(PlugDesc.Path, ''OmniPlex and MAP Offline SDK Bundle'');' ...
481 'unzip(fullfile(d, ''Matlab Offline Files SDK.zip''), PlugDesc.Path);' ...
482 'file_delete(d,1,3);'];
483 PlugDesc(end).InstalledFcn = ['if (exist(''mexPlex'', ''file'') ~= 3), d = pwd;' ...
484 'cd(fullfile(fileparts(which(''plx_info'')), ''mexPlex''));', ...
485 'build_and_verify_mexPlex; cd(d); end'];
486
487 % === I/O: PLOTLY ===
488 PlugDesc(end+1) = GetStruct('plotly');
489 PlugDesc(end).Version = 'github-master';
490 PlugDesc(end).Category = 'I/O';
491 PlugDesc(end).URLzip = 'https://github.com/plotly/plotly_matlab/archive/master.zip';
492 PlugDesc(end).URLinfo = 'https://plotly.com/matlab/';
493 PlugDesc(end).TestFile = 'plotlysetup_online.m';
494 PlugDesc(end).ReadmeFile = 'README.mkdn';
495 PlugDesc(end).CompiledStatus = 0;
496 PlugDesc(end).LoadFolders = {'*'};
497 PlugDesc(end).ExtraMenus = {'Online tutorial', 'web(''https://neuroimage.usc.edu/brainstorm/Tutorials/Plotly'', ''-browser'')'};
498
499 % === I/O: TDT-SDK === Tucker-Davis Technologies Matlab SDK
500 PlugDesc(end+1) = GetStruct('tdt-sdk');
501 PlugDesc(end).Version = 'latest';
502 PlugDesc(end).Category = 'I/O';
503 PlugDesc(end).URLzip = 'https://www.tdt.com/files/examples/TDTMatlabSDK.zip';
504 PlugDesc(end).URLinfo = 'https://www.tdt.com/support/matlab-sdk/';
505 PlugDesc(end).TestFile = 'TDT_Matlab_Tools.pdf';
506 PlugDesc(end).CompiledStatus = 0;
507 PlugDesc(end).LoadFolders = {'*'};
508
509 % === I/O: XDF ===
510 PlugDesc(end+1) = GetStruct('xdf');
511 PlugDesc(end).Version = 'github-master';
512 PlugDesc(end).Category = 'I/O';
513 PlugDesc(end).AutoUpdate = 0;
514 PlugDesc(end).URLzip = 'https://github.com/xdf-modules/xdf-Matlab/archive/refs/heads/master.zip';
515 PlugDesc(end).URLinfo = 'https://github.com/xdf-modules/xdf-Matlab';
516 PlugDesc(end).TestFile = 'load_xdf.m';
517 PlugDesc(end).ReadmeFile = 'readme.md';
518 PlugDesc(end).CompiledStatus = 2;
519
520 % === SIMULATION: SIMMEEG ===
521 PlugDesc(end+1) = GetStruct('simmeeg');
522 PlugDesc(end).Version = 'github-master';
523 PlugDesc(end).Category = 'Simulation';
524 PlugDesc(end).AutoUpdate = 1;
525 PlugDesc(end).URLzip = 'https://github.com/branelab/SimMEEG/archive/master.zip';
526 PlugDesc(end).URLinfo = 'https://audiospeech.ubc.ca/research/brane/brane-lab-software/';
527 PlugDesc(end).TestFile = 'SimMEEG_GUI.m';
528 PlugDesc(end).ReadmeFile = 'SIMMEEG_TERMS_OF_USE.txt';
529 PlugDesc(end).CompiledStatus = 0;
530 PlugDesc(end).DownloadedFcn = ['file_copy( fullfile(PlugDesc.Path, ''SimMEEG-master'', ''bst_simmeeg_new.m''), ' ...
531 'fullfile(PlugDesc.Path, ''SimMEEG-master'', ''bst_simmeeg.m''));' ];
532 PlugDesc(end).RequiredPlugs = {'fieldtrip', '20200911'};
533
534
535 % === STATISTICS: FASTICA ===
536 PlugDesc(end+1) = GetStruct('fastica');
537 PlugDesc(end).Version = '2.5';
538 PlugDesc(end).Category = 'Statistics';
539 PlugDesc(end).URLzip = 'https://research.ics.aalto.fi/ica/fastica/code/FastICA_2.5.zip';
540 PlugDesc(end).URLinfo = 'https://research.ics.aalto.fi/ica/fastica/';
541 PlugDesc(end).TestFile = 'fastica.m';
542 PlugDesc(end).ReadmeFile = 'Contents.m';
543 PlugDesc(end).CompiledStatus = 2;
544
545 % === STATISTICS: LIBSVM ===
546 PlugDesc(end+1) = GetStruct('libsvm');
547 PlugDesc(end).Version = 'github-master';
548 PlugDesc(end).Category = 'Statistics';
549 PlugDesc(end).URLzip = 'https://github.com/cjlin1/libsvm/archive/master.zip';
550 PlugDesc(end).URLinfo = 'https://www.csie.ntu.edu.tw/~cjlin/libsvm/';
551 PlugDesc(end).TestFile = 'svm.cpp';
552 PlugDesc(end).ReadmeFile = 'README';
553 PlugDesc(end).MinMatlabVer = 803; % 2014a
554 PlugDesc(end).CompiledStatus = 2;
555 PlugDesc(end).LoadFolders = {'*'};
556 PlugDesc(end).InstalledFcn = 'd=pwd; cd(fileparts(which(''make''))); make; cd(d);';
557
558 % === STATISTICS: mTRF ===
559 PlugDesc(end+1) = GetStruct('mtrf');
560 PlugDesc(end).Version = '2.4';
561 PlugDesc(end).Category = 'Statistics';
562 PlugDesc(end).URLzip = 'https://github.com/mickcrosse/mTRF-Toolbox/archive/refs/tags/v2.4.zip';
563 PlugDesc(end).URLinfo = 'https://github.com/mickcrosse/mTRF-Toolbox';
564 PlugDesc(end).TestFile = 'mTRFtrain.m';
565 PlugDesc(end).ReadmeFile = 'README.md';
566 PlugDesc(end).CompiledStatus = 0;
567 PlugDesc(end).LoadFolders = {'mtrf'};
568 PlugDesc(end).DeleteFiles = {'.gitattributes', '.github/ISSUE_TEMPLATE', 'data', 'doc', 'examples', 'img'};
569
570 % === STATISTICS: MVGC ===
571 PlugDesc(end+1) = GetStruct('mvgc');
572 PlugDesc(end).Version = 'github-master';
573 PlugDesc(end).Category = 'Statistics';
574 PlugDesc(end).URLzip = 'https://github.com/brainstorm-tools/MVGC1/archive/refs/heads/master.zip';
575 PlugDesc(end).URLinfo = 'https://github.com/brainstorm-tools/MVGC1';
576 PlugDesc(end).TestFile = 'startup_mvgc.m';
577 PlugDesc(end).ReadmeFile = 'README.md';
578 PlugDesc(end).CompiledStatus = 2;
579 PlugDesc(end).LoadFolders = {''};
580 PlugDesc(end).DeleteFiles = {'C', 'deprecated', 'utils/legacy', 'maintainer'};
581 PlugDesc(end).DownloadedFcn = ['file_move( fullfile(PlugDesc.Path, ''MVGC1-master'', ''startup.m''), ' ...
582 'fullfile(PlugDesc.Path, ''MVGC1-master'', ''startup_mvgc.m''));' ...
583 'file_delete(fullfile(PlugDesc.Path, ''MVGC1-master'', ''demo'', ''mvgc_demo.m''), 1);', ...
584 'file_copy( fullfile(PlugDesc.Path, ''MVGC1-master'', ''demo'', ''mvgc_demo_statespace.m''), ' ...
585 'fullfile(PlugDesc.Path, ''MVGC1-master'', ''demo'', ''mvgc_demo.m''));' ];
586 PlugDesc(end).LoadedFcn = 'startup_mvgc;';
587
588 % === STATISTICS: PICARD ===
589 PlugDesc(end+1) = GetStruct('picard');
590 PlugDesc(end).Version = 'github-master';
591 PlugDesc(end).Category = 'Statistics';
592 PlugDesc(end).URLzip = 'https://github.com/pierreablin/picard/archive/refs/heads/master.zip';
593 PlugDesc(end).URLinfo = 'https://github.com/pierreablin/picard';
594 PlugDesc(end).TestFile = 'picard.m';
595 PlugDesc(end).ReadmeFile = 'README.rst';
596 PlugDesc(end).CompiledStatus = 2;
597 PlugDesc(end).LoadFolders = {'matlab_octave'};
598
599 % === ELECTROPHYSIOLOGY: DERIVELFP ===
600 PlugDesc(end+1) = GetStruct('derivelfp');
601 PlugDesc(end).Version = '1.0';
602 PlugDesc(end).Category = 'e-phys';
603 PlugDesc(end).AutoUpdate = 0;
604 PlugDesc(end).URLzip = 'https://packlab.mcgill.ca/despikingtoolbox.zip';
605 PlugDesc(end).URLinfo = 'https://journals.physiology.org/doi/full/10.1152/jn.00642.2010';
606 PlugDesc(end).TestFile = 'despikeLFP.m';
607 PlugDesc(end).ReadmeFile = 'readme.txt';
608 PlugDesc(end).CompiledStatus = 2;
609 PlugDesc(end).LoadFolders = {'toolbox'};
610 PlugDesc(end).DeleteFiles = {'ExampleDespiking.m', 'appendixpaper.pdf', 'downsample2x.m', 'examplelfpdespiking.mat', 'sta.m', ...
611 'toolbox/delineSignal.m', 'toolbox/despikeLFPbyChunks.asv', 'toolbox/despikeLFPbyChunks.m'};
612
613 % === ELECTROPHYSIOLOGY: Kilosort ===
614 PlugDesc(end+1) = GetStruct('kilosort');
615 PlugDesc(end).Version = 'github-master';
616 PlugDesc(end).Category = 'e-phys';
617 PlugDesc(end).URLzip = 'https://github.com/cortex-lab/KiloSort/archive/refs/heads/master.zip';
618 PlugDesc(end).URLinfo = 'https://papers.nips.cc/paper/2016/hash/1145a30ff80745b56fb0cecf65305017-Abstract.html';
619 PlugDesc(end).TestFile = 'fitTemplates.m';
620 PlugDesc(end).ReadmeFile = 'readme.md';
621 PlugDesc(end).CompiledStatus = 0;
622 PlugDesc(end).LoadFolders = {'*'};
623 PlugDesc(end).RequiredPlugs = {'kilosort-wrapper'; 'phy'; 'npy-matlab'};
624 PlugDesc(end).InstalledFcn = 'process_spikesorting_kilosort(''copyKilosortConfig'', bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''KiloSort-master'', ''configFiles'', ''StandardConfig_MOVEME.m''), bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''KiloSort-master'', ''KilosortStandardConfig.m''));';
625
626
627 % === ELECTROPHYSIOLOGY: Kilosort Wrapper ===
628 PlugDesc(end+1) = GetStruct('kilosort-wrapper');
629 PlugDesc(end).Version = 'github-master';
630 PlugDesc(end).Category = 'e-phys';
631 PlugDesc(end).URLzip = 'https://github.com/brendonw1/KilosortWrapper/archive/refs/heads/master.zip';
632 PlugDesc(end).URLinfo = 'https://zenodo.org/record/3604165';
633 PlugDesc(end).TestFile = 'Kilosort2Neurosuite.m';
634 PlugDesc(end).ReadmeFile = 'README.md';
635 PlugDesc(end).CompiledStatus = 0;
636
637 % === ELECTROPHYSIOLOGY: phy ===
638 PlugDesc(end+1) = GetStruct('phy');
639 PlugDesc(end).Version = 'github-master';
640 PlugDesc(end).Category = 'e-phys';
641 PlugDesc(end).URLzip = 'https://github.com/cortex-lab/phy/archive/refs/heads/master.zip';
642 PlugDesc(end).URLinfo = 'https://phy.readthedocs.io/en/latest/';
643 PlugDesc(end).TestFile = 'feature_view_custom_grid.py';
644 PlugDesc(end).LoadFolders = {'*'};
645 PlugDesc(end).ReadmeFile = 'README.md';
646 PlugDesc(end).CompiledStatus = 0;
647 PlugDesc(end).RequiredPlugs = {'npy-matlab'};
648
649 % === ELECTROPHYSIOLOGY: ultramegasort2000 ===
650 PlugDesc(end+1) = GetStruct('ultramegasort2000');
651 PlugDesc(end).Version = 'github-master';
652 PlugDesc(end).Category = 'e-phys';
653 PlugDesc(end).URLzip = 'https://github.com/danamics/UMS2K/archive/refs/heads/master.zip';
654 PlugDesc(end).URLinfo = 'https://github.com/danamics/UMS2K/blob/master/UltraMegaSort2000%20Manual.pdf';
655 PlugDesc(end).TestFile = 'UltraMegaSort2000 Manual.pdf';
656 PlugDesc(end).LoadFolders = {'*'};
657 PlugDesc(end).ReadmeFile = 'README.md';
658 PlugDesc(end).CompiledStatus = 0;
659
660 % === ELECTROPHYSIOLOGY: waveclus ===
661 PlugDesc(end+1) = GetStruct('waveclus');
662 PlugDesc(end).Version = 'github-master';
663 PlugDesc(end).Category = 'e-phys';
664 PlugDesc(end).URLzip = 'https://github.com/csn-le/wave_clus/archive/refs/heads/master.zip';
665 PlugDesc(end).URLinfo = 'https://journals.physiology.org/doi/full/10.1152/jn.00339.2018';
666 PlugDesc(end).TestFile = 'wave_clus.m';
667 PlugDesc(end).LoadFolders = {'*'};
668 PlugDesc(end).ReadmeFile = 'README.md';
669 PlugDesc(end).CompiledStatus = 0;
670
671 % === EVENTS: CTAGGER ===
672 PlugDesc(end+1) = GetStruct('ctagger');
673 PlugDesc(end).Version = 'github-main';
674 PlugDesc(end).Category = 'Events';
675 PlugDesc(end).AutoUpdate = 0;
676 PlugDesc(end).CompiledStatus = 0;
677 PlugDesc(end).URLzip = 'https://github.com/hed-standard/CTagger/archive/main.zip';
678 PlugDesc(end).URLinfo = 'https://www.hed-resources.org/en/latest/CTaggerGuiTaggingTool.html';
679 PlugDesc(end).ReadmeFile = 'README.md';
680 PlugDesc(end).MinMatlabVer = 803; % 2014a
681 PlugDesc(end).LoadFolders = {'*'};
682 PlugDesc(end).LoadedFcn = @Configure;
683 PlugDesc(end).TestFile = 'CTagger.jar';
684 PlugDesc(end).DeleteFiles = {'assets', 'gradle', 'src', '.gradle', '.github', '.vscode', ...
685 'build.gradle', 'gradle.properties', 'gradlew', 'gradlew.bat', ...
686 'settings.gradle', '.codeclimate.yml', '.gitignore'};
687
688
689 % === fNIRS: NIRSTORM ===
690 PlugDesc(end+1) = GetStruct('nirstorm');
691 PlugDesc(end).Version = 'github-master';
692 PlugDesc(end).Category = 'fNIRS';
693 PlugDesc(end).AutoUpdate = 0;
694 PlugDesc(end).AutoLoad = 1;
695 PlugDesc(end).CompiledStatus = 2;
696 PlugDesc(end).URLzip = 'https://github.com/Nirstorm/nirstorm/archive/master.zip';
697 PlugDesc(end).URLinfo = 'https://github.com/Nirstorm/nirstorm';
698 PlugDesc(end).LoadFolders = {'bst_plugin/core','bst_plugin/forward','bst_plugin/GLM', 'bst_plugin/inverse' , 'bst_plugin/io','bst_plugin/math' ,'bst_plugin/mbll' ,'bst_plugin/misc', 'bst_plugin/OM', 'bst_plugin/preprocessing', 'bst_plugin/ppl'};
699 PlugDesc(end).TestFile = 'process_nst_mbll.m';
700 PlugDesc(end).ReadmeFile = 'README.md';
701 PlugDesc(end).GetVersionFcn = 'nst_get_version';
702 PlugDesc(end).RequiredPlugs = {'brainentropy'};
703 PlugDesc(end).MinMatlabVer = 803; % 2014a
704 PlugDesc(end).DeleteFiles = {'scripts', 'test', 'run_tests.m', 'test_suite_bak.m', '.gitignore'};
705
706 % === fNIRS: MCXLAB CUDA ===
707 PlugDesc(end+1) = GetStruct('mcxlab-cuda');
708 PlugDesc(end).Version = '2024.07.23';
709 PlugDesc(end).Category = 'fNIRS';
710 PlugDesc(end).AutoUpdate = 1;
711 PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlab-allinone-git20240723.zip';
712 PlugDesc(end).TestFile = 'mcxlab.m';
713 PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
714 PlugDesc(end).CompiledStatus = 0;
715 PlugDesc(end).LoadFolders = {'*'};
716 PlugDesc(end).UnloadPlugs = {'mcxlab-cl'};
717
718 % === fNIRS: MCXLAB CL ===
719 PlugDesc(end+1) = GetStruct('mcxlab-cl');
720 PlugDesc(end).Version = '2024.07.23';
721 PlugDesc(end).Category = 'fNIRS';
722 PlugDesc(end).AutoUpdate = 0;
723 PlugDesc(end).URLzip = 'https://mcx.space/nightly/release/git20240723/mcxlabcl-allinone-git20240723.zip';
724 PlugDesc(end).TestFile = 'mcxlabcl.m';
725 PlugDesc(end).URLinfo = 'https://mcx.space/wiki/';
726 PlugDesc(end).CompiledStatus = 2;
727 PlugDesc(end).LoadFolders = {'*'};
728 PlugDesc(end).UnloadPlugs = {'mcxlab-cuda'};
729
730 % === sEEG: GARDEL ===
731 PlugDesc(end+1) = GetStruct('gardel');
732 PlugDesc(end).Version = 'latest';
733 PlugDesc(end).Category = 'sEEG';
734 PlugDesc(end).URLzip = 'https://gitlab-dynamap.timone.univ-amu.fr/public_shared_tools/gardel/-/archive/GARDEL_Brainstorm/gardel-GARDEL_Brainstorm.zip';
735 PlugDesc(end).URLinfo = 'https://gitlab-dynamap.timone.univ-amu.fr/public_shared_tools/gardel/-/wikis/home';
736 PlugDesc(end).TestFile = 'code/Functions/elec_auto_segmentation.m';
737 PlugDesc(end).ReadmeFile = 'README.md';
738 PlugDesc(end).CompiledStatus = 2;
739 PlugDesc(end).RequiredPlugs = {'spm12'};
740 PlugDesc(end).LoadFolders = {'*'};
741 PlugDesc(end).InstalledFcn = {};
742 PlugDesc(end).DeleteFiles = {'gardel_splash.png', 'gardel_splash_480x480.png', 'gardel_splash480x480.svg', ...
743 'code/Functions/associate_matter.m', 'code/Functions/associate_region.m', 'code/Functions/BN_colorlut2complete_descr.m', ...
744 'code/Functions/coregistration.m', 'code/Functions/find_parsing_json.m', 'code/Functions/First_SPM_reorientation.m', ...
745 'code/Functions/make_bids_filename.m', 'code/Functions/new_brainmasking_to_be_compiled.m', 'code/Functions/NEW_SEG2.m', ...
746 'code/Functions/open_electrodes_file.m', 'code/Functions/open_nii_anatomical_convention.m', 'code/Functions/plot_brain.m', ...
747 'code/Functions/read_BNatlas.m', 'code/Functions/read_fscolorlut.m', 'code/Functions/Segmentation.asv', 'code/Functions/stlwrite.m', ...
748 'code/Functions/write_coordsystem_json.m', 'code/Functions/write2Brainstorm.m', 'code/Functions/write2MNI.m', ...
749 'code/toolboxes/NIFTI_dataViewer', 'code/toolboxes/freezeColors', 'code/toolboxes/dicm2nii', ...
750 'code/.gitkeep', 'code/BN_Atlas_246_LUT_sam.txt', 'code/CTthresholdGUI.fig', 'code/CTthresholdGUI.m', 'code/electrode_creation.m', ...
751 'code/electrodes_profiles.json', 'code/GARDEL User Manual_V2.docx', 'code/GARDEL User Manual_V2.pdf', 'code/GARDEL.fig', ...
752 'code/GARDEL.m', 'code/GARDELv2_bis.prj', 'code/MontageAnyWaveGardel.m', 'code/newtabMarsAtlas_SAM_2016.csv', ...
753 'code/save_bipolar_with_region.m', 'code/single_subj_T1.nii', 'code/VepFreeSurferColorLut.txt', 'code/write_localisations.m'};
754
755 % === sEEG: MIA ===
756 PlugDesc(end+1) = GetStruct('mia');
757 PlugDesc(end).Version = 'github-master';
758 PlugDesc(end).Category = 'sEEG';
759 PlugDesc(end).AutoUpdate = 0;
760 PlugDesc(end).AutoLoad = 1;
761 PlugDesc(end).CompiledStatus = 2;
762 PlugDesc(end).URLzip = 'https://github.com/MIA-iEEG/mia/archive/refs/heads/master.zip';
763 PlugDesc(end).URLinfo = 'https://www.neurotrack.fr/mia/';
764 PlugDesc(end).ReadmeFile = 'README.md';
765 PlugDesc(end).MinMatlabVer = 803; % 2014a
766 PlugDesc(end).LoadFolders = {'*'};
767 PlugDesc(end).TestFile = 'process_mia_export_db.m';
768 PlugDesc(end).ExtraMenus = {'Start MIA', 'mia', 'loaded'};
769
770 % === FIELDTRIP ===
771 PlugDesc(end+1) = GetStruct('fieldtrip');
772 PlugDesc(end).Version = 'latest';
773 PlugDesc(end).AutoUpdate = 0;
774 PlugDesc(end).URLzip = 'https://download.fieldtriptoolbox.org/fieldtrip-lite-20240405.zip';
775 PlugDesc(end).URLinfo = 'https://www.fieldtriptoolbox.org';
776 PlugDesc(end).TestFile = 'ft_defaults.m';
777 PlugDesc(end).ReadmeFile = 'README';
778 PlugDesc(end).CompiledStatus = 2;
779 PlugDesc(end).UnloadPlugs = {'spm12', 'roast'};
780 PlugDesc(end).LoadFolders = {'specest', 'preproc', 'forward', 'src', 'utilities'};
781 PlugDesc(end).GetVersionFcn = 'ft_version';
782 PlugDesc(end).LoadedFcn = ['global ft_default; ' ...
783 'ft_default = []; ' ...
784 'clear ft_defaults; ' ...
785 'clear global defaults; ', ...
786 'if exist(''filtfilt'', ''file''), ft_default.toolbox.signal=''matlab''; end; ' ...
787 'if exist(''nansum'', ''file''), ft_default.toolbox.stats=''matlab''; end; ' ...
788 'if exist(''rgb2hsv'', ''file''), ft_default.toolbox.images=''matlab''; end; ' ...
789 'ft_defaults;'];
790
791 % === SPM12 ===
792 PlugDesc(end+1) = GetStruct('spm12');
793 PlugDesc(end).Version = 'latest';
794 PlugDesc(end).AutoUpdate = 0;
795 switch(OsType)
796 case 'mac64arm'
797 PlugDesc(end).URLzip = 'https://github.com/spm/spm12/archive/refs/heads/maint.zip';
798 PlugDesc(end).Version = 'github-maint';
799 otherwise
800 PlugDesc(end).Version = 'latest';
801 PlugDesc(end).URLzip = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip';
802 end
803 PlugDesc(end).URLinfo = 'https://www.fil.ion.ucl.ac.uk/spm/';
804 PlugDesc(end).TestFile = 'spm.m';
805 PlugDesc(end).ReadmeFile = 'README.md';
806 PlugDesc(end).CompiledStatus = 2;
807 PlugDesc(end).UnloadPlugs = {'fieldtrip', 'roast'};
808 PlugDesc(end).LoadFolders = {'matlabbatch'};
809 PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @spm, ''Ver'')';
810 PlugDesc(end).LoadedFcn = 'spm(''defaults'',''EEG'');';
811
812 % === USER DEFINED PLUGINS ===
813 plugJsonFiles = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
814 badJsonFiles = {};
815 plugUserDefNames = {};
816 for ix = 1:length(plugJsonFiles)
817 plugJsonText = fileread(fullfile(plugJsonFiles(ix).folder, plugJsonFiles(ix).name));
818 try
819 PlugUserDesc = bst_jsondecode(plugJsonText);
820 catch
821 badJsonFiles{end+1} = plugJsonFiles(ix).name;
822 continue
823 end
824 % Reshape fields "ExtraMenus"
825 if isfield(PlugUserDesc, 'ExtraMenus') && ~isempty(PlugUserDesc.ExtraMenus) && iscell(PlugUserDesc.ExtraMenus{1})
826 PlugUserDesc.ExtraMenus = cat(2, PlugUserDesc.ExtraMenus{:})';
827 end
828 % Reshape fields "RequiredPlugs"
829 if isfield(PlugUserDesc, 'RequiredPlugs') && ~isempty(PlugUserDesc.RequiredPlugs) && iscell(PlugUserDesc.RequiredPlugs{1})
830 PlugUserDesc.RequiredPlugs = cat(2, PlugUserDesc.RequiredPlugs{:})';
831 end
832 % Check for uniqueness for user-defined plugin
833 if ~ismember(PlugUserDesc.Name, {PlugDesc.Name})
834 plugUserDefNames{end+1} = PlugUserDesc.Name;
835 PlugDesc(end+1) = struct_copy_fields(GetStruct(PlugUserDesc.Name), PlugUserDesc);
836 end
837 end
838 % Print info on user-defined plugins
839 if UserDefVerbose
840 if ~isempty(plugUserDefNames)
841 fprintf(['BST> User-defined plugins... ' strjoin(plugUserDefNames, ' ') '\n']);
842 end
843 for iBad = 1 : length(badJsonFiles)
844 fprintf(['BST> User-defined plugins, error reading .json file... ' badJsonFiles{iBad} '\n']);
845 end
846 end
847
848 % ================================================================================================================
849
850 % Select only one plugin
851 if ~isempty(SelPlug)
852 % Get plugin name
853 if ischar(SelPlug)
854 PlugName = SelPlug;
855 else
856 PlugName = SelPlug.Name;
857 end
858 % Find in the list of plugins
859 iPlug = find(strcmpi({PlugDesc.Name}, PlugName));
860 if ~isempty(iPlug)
861 PlugDesc = PlugDesc(iPlug);
862 else
863 PlugDesc = [];
864 end
865 end
866 end
867
868
869 %% ===== PLUGIN STRUCT =====
870 function s = GetStruct(PlugName)
871 s = db_template('PlugDesc');
872 s.Name = PlugName;
873 end
874
875
876 %% ===== ADD USER DEFINED PLUGIN DESCRIPTION =====
877 function [isOk, errMsg] = AddUserDefDesc(RegMethod, jsonLocation)
878 isOk = 1;
879 errMsg = '';
880 isInteractive = strcmp(RegMethod, 'manual') || nargin < 2 || isempty(jsonLocation);
881
882 % Get json file location from user
883 if ismember(RegMethod, {'file', 'url'}) && isInteractive
884 if strcmp(RegMethod, 'file')
885 jsonLocation = java_getfile('open', 'Plugin description JSON file...', '', 'single', 'files', {{'.json'}, 'Brainstorm plugin description (*.json)', 'JSON'}, 1);
886 elseif strcmp(RegMethod, 'url')
887 jsonLocation = java_dialog('input', 'Enter the URL the plugin description file (.json)', 'Plugin description JSON file...', [], '');
888 end
889 if isempty(jsonLocation)
890 return
891 end
892 res = java_dialog('question', ['Warning: This plugin has not been verified.' 10 ...
893 'Malicious plugins can alter your database, proceed with caution and only install plugins from trusted sources.' 10 ...
894 'If any unusual behavior occurs after installation, start by uninstalling the plugins.' 10 ...
895 'Are you sure you want to proceed?'], ...
896 'Warning', [], {'yes', 'no'});
897 if strcmp(res, 'no')
898 return
899 end
900 end
901
902 % Get plugin description
903 switch RegMethod
904 case 'file'
905 jsonText = fileread(jsonLocation);
906 try
907 PlugDesc = bst_jsondecode(jsonText);
908 catch
909 errMsg = sprintf(['Could not parse JSON file:' 10 '%s'], jsonLocation);
910 end
911
912 case 'url'
913 % Handle GitHub links, convert the link to load the raw content
914 if strcmp(jsonLocation(1:4),'http') && strcmp(jsonLocation(end-4:end),'.json')
915 if ~isempty(regexp(jsonLocation, '^http[s]*://github.com', 'once'))
916 jsonLocation = strrep(jsonLocation, 'github.com','raw.githubusercontent.com');
917 jsonLocation = strrep(jsonLocation, 'blob/', '');
918 end
919 end
920 jsonText = bst_webread(jsonLocation);
921 try
922 PlugDesc = bst_jsondecode(jsonText);
923 catch
924 errMsg = sprintf(['Could not parse JSON file at:' 10 '%s'], jsonLocation);
925 end
926
927 case 'manual'
928 % Get info for user-defined plugin description from user
929 res = java_dialog('input', { ['<HTML>Provide the <B>mandatory</B> fields for a user defined Brainstorm plugin<BR>' ...
930 'See this page for further details:<BR>' ...
931 '<FONT COLOR="#0000FF">https://neuroimage.usc.edu/brainstorm/Tutorials/Plugins</FONT>' ...
932 '<BR><BR>' ...
933 'Plugin name<BR>' ...
934 '<I><FONT color="#707070">EXAMPLE: bst-users</FONT></I>'], ...
935 ['<HTML>Version<BR>' ...
936 '<I><FONT color="#707070">EXAMPLE: github-main or 3.1.4</FONT></I>'], ...
937 ['<HTML>URL for zip<BR>' ...
938 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users/archive/refs/heads/master.zip</FONT></I>'], ...
939 ['<HTML>URL for information<BR>' ...
940 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users</FONT></I>']}, ...
941 'User defined plugin', [], {'', '', '', ''});
942 if isempty(res) || any(cellfun(@isempty,res))
943 return
944 end
945 PlugDesc.Name = lower(res{1});
946 PlugDesc.Version = res{2};
947 PlugDesc.URLzip = res{3};
948 PlugDesc.URLinfo = res{4};
949 end
950 if ~isempty(errMsg)
951 bst_error(errMsg);
952 isOk = 0;
953 return;
954 end
955
956 % Validate retrieved plugin description
957 if length(PlugDesc) > 1
958 errMsg = 'JSON file should contain only one plugin description';
959 elseif ~all(ismember({'Name', 'Version', 'URLzip', 'URLinfo'}, fieldnames(PlugDesc)))
960 errMsg = 'Plugin description must contain the fields ''Name'', ''Version'', ''URLzip'' and ''URLinfo''';
961 else
962 PlugDesc.Name = lower(PlugDesc.Name);
963 PlugDescs = GetSupported();
964 if ismember(PlugDesc.Name, {PlugDescs.Name})
965 errMsg = sprintf('Plugin ''%s'' already exist in Brainstorm', PlugDesc.Name);
966 end
967 end
968 if ~isempty(errMsg)
969 bst_error(errMsg);
970 isOk = 0;
971 return;
972 end
973 % Override category
974 PlugDesc.Category = 'User defined';
975 % Keep only valid fields
976 fieldsToDel = setdiff(fieldnames(PlugDesc), fieldnames(db_template('plugdesc')));
977 PlugDesc = rmfield(PlugDesc, fieldsToDel);
978
979 % Write validated JSON file
980 pluginJsonFileOut = fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name)));
981 fid = fopen(pluginJsonFileOut, 'wt');
982 jsonText = bst_jsonencode(PlugDesc, 1);
983 fprintf(fid, jsonText);
984 fclose(fid);
985
986 fprintf(1, 'BST> Plugin ''%s'' was added to ''User defined'' plugins\n', PlugDesc.Name);
987 end
988
989
990 %% ===== REMOVE USER DEFINED PLUGIN DESCRIPTION =====
991 function [isOk, errMsg] = RemoveUserDefDesc(PlugName)
992 isOk = 1;
993 errMsg = '';
994 if nargin < 1 || isempty(PlugName)
995 PlugDescs = GetSupported();
996 PlugDescs = PlugDescs(ismember({PlugDescs.Category}, 'User defined'));
997 PlugName = java_dialog('combo', 'Indicate the name of the plugin to remove:', 'Remove plugin from ''User defined'' list', [], {PlugDescs.Name});
998 end
999 if isempty(PlugName)
1000 return
1001 end
1002 PlugDesc = GetSupported(PlugName);
1003 if ~isempty(PlugDesc.Path) || file_exist(bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name))
1004 [isOk, errMsg] = Uninstall(PlugDesc.Name, 0);
1005 end
1006 % Delete json file
1007 if isOk
1008 isOk = file_delete(fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name))), 1);
1009 end
1010
1011 fprintf(1, 'BST> Plugin ''%s'' was removed from ''User defined'' plugins\n', PlugDesc.Name);
1012 end
1013
1014
1015 %% ===== CONFIGURE PLUGIN =====
1016 function Configure(PlugDesc)
1017 switch (PlugDesc.Name)
1018 case 'mff'
1019 % Add .jar file to static classpath
1020 if ~exist('com.egi.services.mff.api.MFFFactory', 'class')
1021 jarList = dir(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'MFF-*.jar'));
1022 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, jarList(1).name);
1023 disp(['BST> Adding to Java classpath: ' jarPath]);
1024 warning off
1025 javaaddpathstatic(jarPath);
1026 javaaddpath(jarPath);
1027 warning on
1028 end
1029
1030 case 'nwb'
1031 % Add .jar file to static classpath
1032 if ~exist('Schema', 'class')
1033 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'jar', 'schema.jar');
1034 disp(['BST> Adding to Java classpath: ' jarPath]);
1035 warning off
1036 javaaddpathstatic(jarPath);
1037 javaaddpath(jarPath);
1038 warning on
1039 schema = Schema();
1040 end
1041 % Go to NWB folder
1042 curDir = pwd;
1043 cd(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder));
1044 % Generate the NWB Schema (must be executed from the NWB folder)
1045 generateCore();
1046 % Restore current directory
1047 cd(curDir);
1048
1049 case 'ctagger'
1050 % Add .jar file to static classpath
1051 if ~exist('TaggerLoader', 'class')
1052 jarList = dir(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'CTagger.jar'));
1053 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, jarList(1).name);
1054 disp(['BST> Adding to Java classpath: ' jarPath]);
1055 warning off
1056 javaaddpathstatic(jarPath);
1057 javaaddpath(jarPath);
1058 warning on
1059 end
1060 end
1061 end
1062
1063
1064 %% ===== GET ONLINE VERSION =====
1065 % Get the latest online version of some plugins
1066 function [Version, URLzip] = GetVersionOnline(PlugName, URLzip, isCache)
1067 global GlobalData;
1068 Version = [];
1069 % Parse inputs
1070 if (nargin < 2) || isempty(URLzip)
1071 URLzip = [];
1072 end
1073 % Use cache by default, to avoid fetching online too many times the same info
1074 if (nargin < 3) || isempty(isCache)
1075 isCache = 1;
1076 end
1077 % No internet: skip
1078 if ~GlobalData.Program.isInternet
1079 return;
1080 end
1081 % Check for existing plugin cache
1082 strCache = matlab.lang.makeValidName([PlugName, '_online_', strrep(date,'-','')]);
1083 if isCache && isfield(GlobalData.Program.PluginCache, strCache) && isfield(GlobalData.Program.PluginCache.(strCache), 'Version')
1084 Version = GlobalData.Program.PluginCache.(strCache).Version;
1085 URLzip = GlobalData.Program.PluginCache.(strCache).URLzip;
1086 return;
1087 end
1088 % Get version online
1089 try
1090 switch (PlugName)
1091 case 'spm12'
1092 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1093 disp(['BST> Checking latest online version for ' PlugName '...']);
1094 s = bst_webread('https://www.fil.ion.ucl.ac.uk/spm/download/spm12_updates/');
1095 if ~isempty(s)
1096 n = regexp(s,'spm12_updates_r(\d.*?)\.zip','tokens','once');
1097 if ~isempty(n) && ~isempty(n{1})
1098 Version = n{1};
1099 end
1100 end
1101 case 'cat12'
1102 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1103 disp(['BST> Checking latest online version for ' PlugName '...']);
1104 s = bst_webread('https://www.neuro.uni-jena.de/cat12/');
1105 if ~isempty(s)
1106 n = regexp(s,'cat12_r(\d.*?)\.zip','tokens');
1107 if ~isempty(n)
1108 Version = max(cellfun(@str2double, [n{:}]));
1109 Version = num2str(Version);
1110 end
1111 end
1112 case 'fieldtrip'
1113 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1114 disp(['BST> Checking latest online version for ' PlugName '...']);
1115 s = bst_webread('https://download.fieldtriptoolbox.org');
1116 if ~isempty(s)
1117 n = regexp(s,'fieldtrip-lite-(\d.*?)\.zip','tokens');
1118 if ~isempty(n)
1119 Version = max(cellfun(@str2double, [n{:}]));
1120 Version = num2str(Version);
1121 URLzip = ['https://download.fieldtriptoolbox.org/fieldtrip-lite-' Version '.zip'];
1122 end
1123 end
1124 case 'duneuro'
1125 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1126 disp(['BST> Checking latest online version for ' PlugName '...']);
1127 str = bst_webread('https://neuroimage.usc.edu/bst/getversion_duneuro.php');
1128 Version = str(1:6);
1129 case 'nirstorm'
1130 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1131 disp(['BST> Checking latest online version for ' PlugName '...']);
1132 str = bst_webread('https://raw.githubusercontent.com/Nirstorm/nirstorm/master/bst_plugin/VERSION');
1133 Version = strtrim(str(9:end));
1134 case 'brainentropy'
1135 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1136 disp(['BST> Checking latest online version for ' PlugName '...']);
1137 str = bst_webread('https://raw.githubusercontent.com/multifunkim/best-brainstorm/master/best/VERSION.txt');
1138 str = strsplit(str,'\n');
1139 Version = strtrim(str{1});
1140 otherwise
1141 % If downloading from GitHub: Get last GitHub commit SHA
1142 if isGithubSnapshot(URLzip)
1143 Version = GetGithubCommit(URLzip);
1144 else
1145 return;
1146 end
1147 end
1148 % Executed only if the version was fetched successfully: Keep cached version
1149 GlobalData.Program.PluginCache.(strCache).Version = Version;
1150 GlobalData.Program.PluginCache.(strCache).URLzip = URLzip;
1151 catch
1152 disp(['BST> Error: Could not get online version for plugin: ' PlugName]);
1153 end
1154 end
1155
1156
1157 %% ===== IS GITHUB SNAPSHOT ======
1158 % Returns 1 if the URL is a souce-code archive or snapshot (as .zip or .tar.gz) of a GitHub repository
1159 % https://docs.github.com/en/repositories/working-with-files/using-files/downloading-source-code-archives
1160 function isOk = isGithubSnapshot(URLzip)
1161 isOk = strMatchEdge(URLzip, 'https://github.com/', 'start') && ...
1162 ~isempty(strfind(URLzip, '/archive/')) && ...
1163 (strMatchEdge(URLzip, '.zip', 'end') || strMatchEdge(URLzip, '.tar.gz', 'end'));
1164 end
1165
1166
1167 %% ===== GET GITHUB COMMIT =====
1168 % Get SHA of the GitHub HEAD commit
1169 function sha = GetGithubCommit(URLzip)
1170 zipUri = matlab.net.URI(URLzip);
1171 % Get reference: branch, tag or commit
1172 [~, gitReference] = bst_fileparts(char(zipUri.Path(end)));
1173 if strMatchEdge(URLzip, '.tar.gz', 'end')
1174 % Remove second file extension
1175 [~, gitReference] = bst_fileparts(gitReference);
1176 end
1177 % Default result
1178 sha = ['github-', gitReference];
1179 % Only available after Matlab 2016b (because of matlab.net.http.RequestMessage)
1180 if (bst_get('MatlabVersion') < 901)
1181 return;
1182 end
1183 % Try getting the SHA from the GitHub API
1184 try
1185 % Get GitHub repository path
1186 zipUri = matlab.net.URI(URLzip);
1187 gitUser = char(zipUri.Path(2));
1188 gitRepo = char(zipUri.Path(3));
1189 % Request last commit SHA with GitHub API
1190 apiUri = matlab.net.URI(['https://api.github.com/repos/' gitUser '/' gitRepo '/commits/' gitReference]);
1191 request = matlab.net.http.RequestMessage;
1192 request = request.addFields(matlab.net.http.HeaderField('Accept', 'application/vnd.github.VERSION.sha'));
1193 r = send(request, apiUri);
1194 sha = char(r.Body.Data);
1195 catch
1196 disp(['BST> Warning: Could not get GitHub version for URL: ' zipUrl]);
1197 end
1198 end
1199
1200
1201 %% ===== COMPARE VERSIONS =====
1202 % Returns: 0: v1==v2
1203 % -1: v1<v2
1204 % 1: v1>v2
1205 function res = CompareVersions(v1, v2)
1206 % Get numbers
1207 iNum1 = find(ismember(v1, '0123456789'));
1208 iNum2 = find(ismember(v2, '0123456789'));
1209 iDot1 = find(v1 == '.');
1210 iDot2 = find(v2 == '.');
1211 % Equality (or one input empty)
1212 if isequal(v1,v2) || isempty(v1) || isempty(v2)
1213 res = 0;
1214 % Only numbers
1215 elseif (length(iNum1) == length(v1)) && (length(iNum2) == length(v2))
1216 n1 = str2double(v1);
1217 n2 = str2double(v2);
1218 if (n1 > n2)
1219 res = 1;
1220 elseif (n1 < n2)
1221 res = -1;
1222 else
1223 res = 0;
1224 end
1225 % Format '1.2.3'
1226 elseif (~isempty(iDot1) || ~isempty(iDot2)) && ~isempty(iNum1) && ~isempty(iNum2)
1227 % Get subversions 1
1228 split1 = str_split(v1, '.');
1229 sub1 = [];
1230 for i = 1:length(split1)
1231 t = str2num(split1{i}(ismember(split1{i},'0123456789')));
1232 if ~isempty(t)
1233 sub1(end+1) = t;
1234 else
1235 break;
1236 end
1237 end
1238 % Get subversions 1
1239 split2 = str_split(v2, '.');
1240 sub2 = [];
1241 for i = 1:length(split2)
1242 t = str2num(split2{i}(ismember(split2{i},'0123456789')));
1243 if ~isempty(t)
1244 sub2(end+1) = t;
1245 else
1246 break;
1247 end
1248 end
1249 % Add extra zeros to the shortest (so that "1.2" is higher than "1")
1250 if (length(sub1) < length(sub2))
1251 tmp = sub1;
1252 sub1 = zeros(size(sub2));
1253 sub1(1:length(tmp)) = tmp;
1254 elseif (length(sub1) > length(sub2))
1255 tmp = sub2;
1256 sub2 = zeros(size(sub1));
1257 sub2(1:length(tmp)) = tmp;
1258 end
1259 % Compare number by number
1260 for i = 1:length(sub1)
1261 if (sub1(i) > sub2(i))
1262 res = 1;
1263 return;
1264 elseif (sub1(i) < sub2(i))
1265 res = -1;
1266 return;
1267 else
1268 res = 0;
1269 end
1270 end
1271 % Mixture of numbers and digits: natural sorting of strings
1272 else
1273 [s,I] = sort_nat({v1, v2});
1274 if (I(1) == 1)
1275 res = -1;
1276 else
1277 res = 1;
1278 end
1279 end
1280 end
1281
1282
1283 %% ===== EXECUTE CALLBACK =====
1284 function [isOk, errMsg] = ExecuteCallback(PlugDesc, f)
1285 isOk = 0;
1286 errMsg = '';
1287 if ~isempty(PlugDesc.(f))
1288 try
1289 if ischar(PlugDesc.(f))
1290 disp(['BST> Executing callback ' f ': ' PlugDesc.(f)]);
1291 eval(PlugDesc.(f));
1292 elseif isa(PlugDesc.(f), 'function_handle')
1293 disp(['BST> Executing callback ' f ': ' func2str(PlugDesc.(f))]);
1294 feval(PlugDesc.(f), PlugDesc);
1295 end
1296 catch
1297 errMsg = ['Error executing callback ' f ': ' 10 lasterr];
1298 return;
1299 end
1300 end
1301 isOk = 1;
1302 end
1303
1304
1305 %% ===== GET INSTALLED PLUGINS =====
1306 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled', PlugName/PlugDesc) % Get one installed plugin
1307 % [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled') % Get all installed plugins
1308 function [PlugDesc, SearchPlugs] = GetInstalled(SelPlug)
1309 % Parse inputs
1310 if (nargin < 1) || isempty(SelPlug)
1311 SelPlug = [];
1312 end
1313
1314 % === DEFINE SEARCH LIST ===
1315 % Looking for a single plugin
1316 if ~isempty(SelPlug)
1317 SearchPlugs = GetSupported(SelPlug);
1318 % Looking for all supported plugins
1319 else
1320 SearchPlugs = GetSupported();
1321 end
1322 % Brainstorm plugin folder
1323 UserPluginsDir = bst_get('UserPluginsDir');
1324 % Custom plugin paths
1325 PluginCustomPath = bst_get('PluginCustomPath');
1326 % Matlab path
1327 matlabPath = str_split(path, pathsep);
1328 % Compiled distribution
1329 isCompiled = bst_iscompiled();
1330
1331 % === LOOK FOR SUPPORTED PLUGINS ===
1332 % Empty plugin structure
1333 PlugDesc = repmat(db_template('PlugDesc'), 0);
1334 % Look for each plugin in the search list
1335 for iSearch = 1:length(SearchPlugs)
1336 % Compiled: skip plugins that are not available
1337 if isCompiled && (SearchPlugs(iSearch).CompiledStatus == 0)
1338 continue;
1339 end
1340 % Theoretical plugin path
1341 PlugName = SearchPlugs(iSearch).Name;
1342 PlugPath = bst_fullfile(UserPluginsDir, PlugName);
1343 % Handle case symbolic link
1344 try
1345 PlugPath = builtin('_canonicalizepath', PlugPath);
1346 catch
1347 % Nothing here
1348 end
1349 % Check if test function is available in the Matlab path
1350 TestFilePath = GetTestFilePath(SearchPlugs(iSearch));
1351 % If installed software found in Matlab path
1352 if ~isempty(TestFilePath)
1353 % Register loaded plugin
1354 iPlug = length(PlugDesc) + 1;
1355 PlugDesc(iPlug) = SearchPlugs(iSearch);
1356 PlugDesc(iPlug).isLoaded = 1;
1357 % Check if the file is inside the Brainstorm user folder (where it is supposed to be) => Managed plugin
1358 if strMatchEdge(TestFilePath, PlugPath, 'start')
1359 PlugDesc(iPlug).isManaged = 1;
1360 % Process compiled together with Brainstorm
1361 elseif isCompiled && ~isempty(strfind(TestFilePath, ['.brainstorm' filesep 'plugins' filesep PlugName]))
1362 compiledDir = ['.brainstorm' filesep 'plugins' filesep PlugName];
1363 iPath = strfind(TestFilePath, compiledDir);
1364 PlugPath = [TestFilePath(1:iPath-2), filesep, compiledDir];
1365 % Otherwise: Custom installation
1366 else
1367 % If the test file was found in a defined subfolder: remove the subfolder from the plugin path
1368 PlugPath = TestFilePath;
1369 for iSub = 1:length(PlugDesc(iPlug).LoadFolders)
1370 subDir = strrep(PlugDesc(iPlug).LoadFolders{iSub}, '/', filesep);
1371 if (length(PlugPath) > length(subDir)) && isequal(PlugPath(end-length(subDir)+1:end), subDir)
1372 PlugPath = PlugPath(1:end - length(subDir) - 1);
1373 break;
1374 end
1375 end
1376 PlugDesc(iPlug).isManaged = 0;
1377 % Look for process_* functions in the process folder
1378 PlugProc = file_find(PlugPath, 'process_*.m', Inf, 0);
1379 if ~isempty(PlugProc)
1380 % Remove absolute path: use only path relative to the plugin Path
1381 PlugDesc(iPlug).Processes = cellfun(@(c)file_win2unix(strrep(c, [PlugPath, filesep], '')), PlugProc, 'UniformOutput', 0);
1382 end
1383 end
1384 PlugDesc(iPlug).Path = PlugPath;
1385 % Plugin installed: Managed by Brainstorm
1386 elseif isdir(PlugPath) && file_exist(bst_fullfile(PlugPath, 'plugin.mat'))
1387 iPlug = length(PlugDesc) + 1;
1388 PlugDesc(iPlug) = SearchPlugs(iSearch);
1389 PlugDesc(iPlug).Path = PlugPath;
1390 PlugDesc(iPlug).isLoaded = 0;
1391 PlugDesc(iPlug).isManaged = 1;
1392 % Plugin installed: Custom path
1393 elseif isfield(PluginCustomPath, PlugName) && ~isempty(PluginCustomPath.(PlugName)) && file_exist(PluginCustomPath.(PlugName))
1394 iPlug = length(PlugDesc) + 1;
1395 PlugDesc(iPlug) = SearchPlugs(iSearch);
1396 PlugDesc(iPlug).Path = PluginCustomPath.(PlugName);
1397 PlugDesc(iPlug).isLoaded = 0;
1398 PlugDesc(iPlug).isManaged = 0;
1399 end
1400 end
1401
1402 % === LOOK FOR UNREFERENCED PLUGINS ===
1403 % Compiled: do not look for unreferenced plugins
1404 if isCompiled
1405 PlugList = [];
1406 % Get a specific unlisted plugin
1407 elseif ~isempty(SelPlug)
1408 % Get plugin name
1409 if ischar(SelPlug)
1410 PlugName = lower(SelPlug);
1411 else
1412 PlugName = SelPlug.Name;
1413 end
1414 % If plugin is already referenced: skip
1415 if ismember(PlugName, {PlugDesc.Name})
1416 PlugList = [];
1417 % Else: Try to get target plugin as unreferenced
1418 else
1419 PlugList = struct('name', PlugName);
1420 end
1421 % Get all folders in Brainstorm plugins folder
1422 else
1423 PlugList = dir(UserPluginsDir);
1424 end
1425 % Process folders containing a plugin.mat file
1426 for iDir = 1:length(PlugList)
1427 % Ignore entry if plugin name is already in list of documented plugins
1428 PlugName = PlugList(iDir).name;
1429 if ismember(PlugName, {PlugDesc.Name})
1430 continue;
1431 end
1432 % Process only folders
1433 PlugDir = bst_fullfile(UserPluginsDir, PlugName);
1434 if ~isdir(PlugDir) || (PlugName(1) == '.')
1435 continue;
1436 end
1437 % Process only folders containing a 'plugin.mat' file
1438 PlugMatFile = bst_fullfile(PlugDir, 'plugin.mat');
1439 if ~file_exist(PlugMatFile)
1440 continue;
1441 end
1442 % If selecting only one plugin
1443 if ~isempty(SelPlug) && ischar(SelPlug) && ~strcmpi(PlugName, SelPlug)
1444 continue;
1445 end
1446 % Add plugin to list
1447 iPlug = length(PlugDesc) + 1;
1448 PlugDesc(iPlug) = GetStruct(PlugList(iDir).name);
1449 PlugDesc(iPlug).Path = PlugDir;
1450 PlugDesc(iPlug).isManaged = 1;
1451 PlugDesc(iPlug).isLoaded = ismember(PlugDir, matlabPath);
1452 end
1453
1454 % === READ PLUGIN.MAT ===
1455 for iPlug = 1:length(PlugDesc)
1456 % Try to load the plugin.mat file in the plugin folder
1457 PlugMatFile = bst_fullfile(PlugDesc(iPlug).Path, 'plugin.mat');
1458 if file_exist(PlugMatFile)
1459 try
1460 PlugMat = load(PlugMatFile);
1461 catch
1462 PlugMat = struct();
1463 end
1464 % Copy fields
1465 excludedFields = {'Name', 'Path', 'isLoaded', 'isManaged', 'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn'};
1466 loadFields = setdiff(fieldnames(db_template('PlugDesc')), excludedFields);
1467 for iField = 1:length(loadFields)
1468 if isfield(PlugMat, loadFields{iField}) && ~isempty(PlugMat.(loadFields{iField}))
1469 PlugDesc(iPlug).(loadFields{iField}) = PlugMat.(loadFields{iField});
1470 end
1471 end
1472 % Check again if plugin is loaded using its Path
1473 if ~PlugDesc(iPlug).isLoaded && ~isempty(PlugDesc(iPlug).Path)
1474 PlugPath = PlugDesc(iPlug).Path;
1475 if ~isempty(PlugDesc(iPlug).SubFolder)
1476 PlugPath = bst_fullfile(PlugPath, PlugDesc.SubFolder);
1477 end
1478 % Handle case symbolic link
1479 try
1480 PlugPath = builtin('_canonicalizepath', PlugPath);
1481 catch
1482 % Nothing here
1483 end
1484 PlugDesc(iPlug).isLoaded = ismember(PlugPath, matlabPath);
1485 end
1486 else
1487 PlugDesc(iPlug).URLzip = [];
1488 end
1489 end
1490 end
1491
1492
1493 %% ===== GET LOADED PLUGINS =====
1494 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetLoaded')
1495 function PlugDesc = GetLoaded()
1496 PlugDesc = GetInstalled();
1497 PlugDesc = PlugDesc([PlugDesc.isLoaded] == 1);
1498 end
1499
1500
1501 %% ===== GET DESCRIPTION =====
1502 % USAGE: [PlugDesc, errMsg] = GetDescription(PlugName/PlugDesc)
1503 function [PlugDesc, errMsg] = GetDescription(PlugName)
1504 % Initialize returned values
1505 errMsg = '';
1506 PlugDesc = [];
1507 % CALL: GetDescription(PlugDesc)
1508 if isstruct(PlugName)
1509 % Add the missing fields
1510 PlugDesc = struct_copy_fields(PlugName, db_template('PlugDesc'), 0);
1511 % CALL: GetDescription(PlugName)
1512 elseif ischar(PlugName)
1513 % Get supported plugins
1514 AllPlugs = GetSupported();
1515 % Find plugin in supported plugins
1516 iPlug = find(strcmpi({AllPlugs.Name}, PlugName));
1517 if isempty(iPlug)
1518 errMsg = ['Unknown plugin: ' PlugName];
1519 return;
1520 end
1521 % Return found plugin
1522 PlugDesc = AllPlugs(iPlug);
1523 else
1524 errMsg = 'Invalid call to GetDescription().';
1525 end
1526 end
1527
1528
1529 %% ===== GET TEST FILE PATH =====
1530 function TestFilePath = GetTestFilePath(PlugDesc)
1531 % If a test file is defined
1532 if ~isempty(PlugDesc.TestFile)
1533 % Try to find the test function in the path
1534 whichTest = which(PlugDesc.TestFile);
1535 % If it was found: use the parent folder
1536 if ~isempty(whichTest)
1537 % Get the test file path
1538 TestFilePath = bst_fileparts(whichTest);
1539 % FieldTrip: Ignore if found embedded in SPM12
1540 if strcmpi(PlugDesc.Name, 'fieldtrip')
1541 p = which('spm.m');
1542 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1543 TestFilePath = [];
1544 end
1545 % SPM12: Ignore if found embedded in ROAST or in FieldTrip
1546 elseif strcmpi(PlugDesc.Name, 'spm12')
1547 p = which('roast.m');
1548 q = which('ft_defaults.m');
1549 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1550 TestFilePath = [];
1551 end
1552 % Iso2mesh: Ignore if found embedded in ROAST
1553 elseif strcmpi(PlugDesc.Name, 'iso2mesh')
1554 p = which('roast.m');
1555 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1556 TestFilePath = [];
1557 end
1558 % jsonlab, jsnirfy and jnifti: Ignore if found embedded in iso2mesh
1559 elseif strcmpi(PlugDesc.Name, 'jsonlab') || strcmpi(PlugDesc.Name, 'jsnirfy') || strcmpi(PlugDesc.Name, 'jnifti')
1560 p = which('iso2meshver.m');
1561 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1562 TestFilePath = [];
1563 end
1564 % easyh5: Ignore if found embedded in iso2mesh or jsonlab
1565 elseif strcmpi(PlugDesc.Name, 'easyh5')
1566 p = which('iso2meshver.m');
1567 q = which('savejson.m');
1568 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1569 TestFilePath = [];
1570 end
1571 end
1572 else
1573 TestFilePath = [];
1574 end
1575 else
1576 TestFilePath = [];
1577 end
1578 end
1579
1580
1581 %% ===== GET README FILE ====
1582 % Get full path to the readme file
1583 function ReadmeFile = GetReadmeFile(PlugDesc)
1584 ReadmeFile = [];
1585 % If readme file is defined in the plugin structure
1586 if ~isempty(PlugDesc.ReadmeFile)
1587 % If full path already set: use it
1588 if file_exist(PlugDesc.ReadmeFile)
1589 ReadmeFile = PlugDesc.ReadmeFile;
1590 % Else: check in the plugin Path/SubFolder
1591 else
1592 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.ReadmeFile);
1593 if file_exist(tmpFile)
1594 ReadmeFile = tmpFile;
1595 elseif ~isempty(PlugDesc.SubFolder)
1596 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.ReadmeFile);
1597 if file_exist(tmpFile)
1598 ReadmeFile = tmpFile;
1599 end
1600 end
1601 end
1602 end
1603 % Search for default readme
1604 if isempty(ReadmeFile)
1605 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_readme.txt']);
1606 if file_exist(tmpFile)
1607 ReadmeFile = tmpFile;
1608 end
1609 end
1610 end
1611
1612
1613 %% ===== GET LOGO FILE ====
1614 % Get full path to the logo file
1615 function LogoFile = GetLogoFile(PlugDesc)
1616 LogoFile = [];
1617 % If logo file is defined in the plugin structure
1618 if ~isempty(PlugDesc.LogoFile)
1619 % If full path already set: use it
1620 if file_exist(PlugDesc.LogoFile)
1621 LogoFile = PlugDesc.LogoFile;
1622 % Else: check in the plugin Path/SubFolder
1623 else
1624 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.LogoFile);
1625 if file_exist(tmpFile)
1626 LogoFile = tmpFile;
1627 elseif ~isempty(PlugDesc.SubFolder)
1628 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.LogoFile);
1629 if file_exist(tmpFile)
1630 LogoFile = tmpFile;
1631 end
1632 end
1633 end
1634 end
1635 % Search for default logo
1636 if isempty(LogoFile)
1637 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.gif']);
1638 if file_exist(tmpFile)
1639 LogoFile = tmpFile;
1640 end
1641 end
1642 if isempty(LogoFile)
1643 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.png']);
1644 if file_exist(tmpFile)
1645 LogoFile = tmpFile;
1646 end
1647 end
1648 end
1649
1650
1651 %% ===== INSTALL =====
1652 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[])
1653 function [isOk, errMsg, PlugDesc] = Install(PlugName, isInteractive, minVersion)
1654 % Returned variables
1655 isOk = 0;
1656 % Parse inputs
1657 if (nargin < 3) || isempty(minVersion)
1658 minVersion = [];
1659 elseif isnumeric(minVersion)
1660 minVersion = num2str(minVersion);
1661 end
1662 if (nargin < 2) || isempty(isInteractive)
1663 isInteractive = 0;
1664 end
1665 if ~ischar(PlugName)
1666 errMsg = 'Invalid call to Install()';
1667 PlugDesc = [];
1668 return;
1669 end
1670 % Backup calling progress bar;
1671 isCallBar = bst_progress('isvisible');
1672 if isCallBar
1673 pBarParams = bst_progress('getbarparams');
1674 end
1675 % Get plugin structure from name
1676 [PlugDesc, errMsg] = GetDescription(PlugName);
1677 if ~isempty(errMsg)
1678 return;
1679 end
1680 % Check if plugin is supported on Apple silicon
1681 OsType = bst_get('OsType', 0);
1682 if strcmpi(OsType, 'mac64arm') && ismember(PlugName, PluginsNotSupportAppleSilicon())
1683 errMsg = ['Plugin ', PlugName ' is not supported on Apple silicon yet.'];
1684 PlugDesc = [];
1685 return;
1686 end
1687 % Check if there is a URL to download
1688 if isempty(PlugDesc.URLzip)
1689 errMsg = ['No download URL for ', OsType, ': ', PlugName ''];
1690 return;
1691 end
1692 % Compiled version
1693 isCompiled = bst_iscompiled();
1694 if isCompiled
1695 % Needed FieldTrip and SPM functions are available in compiled version of Brainstorm. See bst_spmtrip.m
1696 if ismember(PlugDesc.Name, {'fieldtrip', 'spm12'})
1697 disp(['BST> Some functions of ' PlugDesc.Name ' are compiled with Brainstorm']);
1698 isOk = 1;
1699 errMsg = [];
1700 return;
1701 end
1702 % Plugin is included in the compiled version
1703 if PlugDesc.CompiledStatus == 0
1704 errMsg = ['Plugin ', PlugName ' is not available in the compiled version of Brainstorm.'];
1705 return;
1706 end
1707 end
1708
1709 % Minimum Matlab version
1710 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
1711 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
1712 errMsg = ['Plugin ', PlugName ' is not supported for versions of Matlab <= ' strMinVer];
1713 return;
1714 end
1715 % Get online update (use existing cache)
1716 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugDesc.URLzip, 1);
1717 if ~isempty(newVersion)
1718 PlugDesc.Version = newVersion;
1719 end
1720 if ~isempty(newURLzip)
1721 PlugDesc.URLzip = newURLzip;
1722 end
1723
1724 % === PROCESS DEPENDENCIES ===
1725 % Check required plugins
1726 if ~isempty(PlugDesc.RequiredPlugs)
1727 bst_progress('text', ['Processing dependencies for ' PlugName '...']);
1728 disp(['BST> Processing dependencies: ' PlugName ' requires: ' sprintf('%s ', PlugDesc.RequiredPlugs{:,1})]);
1729 % Get the list of plugins that need to be installed
1730 installPlugs = {};
1731 installVer = {};
1732 strInstall = '';
1733 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
1734 PlugCheck = GetInstalled(PlugDesc.RequiredPlugs{iPlug,1});
1735 % Plugin not install: Install it
1736 if isempty(PlugCheck)
1737 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1738 installVer{end+1} = [];
1739 strInstall = [strInstall, '<B>' installPlugs{end} '</B> '];
1740 % Plugin installed: check version
1741 elseif (size(PlugDesc.RequiredPlugs,2) == 2)
1742 minVerDep = PlugDesc.RequiredPlugs{iPlug,2};
1743 if ~isempty(minVerDep) && (CompareVersions(minVerDep, PlugCheck.Version) > 0)
1744 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1745 installVer{end+1} = PlugDesc.RequiredPlugs{iPlug,2};
1746 strInstall = [strInstall, '<B>' installPlugs{end} '</B>(' installVer{end} ') '];
1747 end
1748 end
1749 end
1750 % If there are plugins to install
1751 if ~isempty(installPlugs)
1752 if isInteractive
1753 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> requires: ' strInstall ...
1754 '<BR><BR>Brainstorm will now install these plugins.' 10 10], 'Plugin manager');
1755 end
1756 for iPlug = 1:length(installPlugs)
1757 [isInstalled, errMsg] = Install(installPlugs{iPlug}, isInteractive, installVer{iPlug});
1758 if ~isInstalled
1759 errMsg = ['Error processing dependency: ' PlugDesc.RequiredPlugs{iPlug,1} 10 errMsg];
1760 return;
1761 end
1762 end
1763 end
1764 end
1765
1766 % === UPDATE: CHECK PREVIOUS INSTALL ===
1767 % Check if installed
1768 OldPlugDesc = GetInstalled(PlugName);
1769 % If already installed
1770 if ~isempty(OldPlugDesc)
1771 % If the plugin is not managed by Brainstorm: do not check versions
1772 if ~OldPlugDesc.isManaged
1773 isUpdate = 0;
1774 % If the requested version is higher
1775 elseif ~isempty(minVersion) && (CompareVersions(minVersion, OldPlugDesc.Version) > 0)
1776 isUpdate = 1;
1777 strUpdate = ['the installed version is outdated.<BR>Minimum version required: <I>' minVersion '</I>'];
1778 % If an update is available and auto-updates are requested
1779 elseif (PlugDesc.AutoUpdate == 1) && bst_get('AutoUpdates') && ... % If updates are enabled
1780 ((isGithubSnapshot(PlugDesc.URLzip) && ~strcmpi(PlugDesc.Version, OldPlugDesc.Version)) || ... % GitHub-master: update if different commit SHA strings
1781 (~isGithubSnapshot(PlugDesc.URLzip) && (CompareVersions(PlugDesc.Version, OldPlugDesc.Version) > 0))) % Regular stable version: update if online version is newer
1782 isUpdate = 1;
1783 strUpdate = 'an update is available online.';
1784 else
1785 isUpdate = 0;
1786 end
1787 % Update plugin
1788 if isUpdate
1789 % Compare versions
1790 strCompare = ['<FONT color="#707070">' ...
1791 'Old version : <I>' OldPlugDesc.Version '</I><BR>' ...
1792 'New version : <I>' PlugDesc.Version '</I></FONT><BR><BR>'];
1793 % Ask user for updating
1794 if isInteractive
1795 isConfirm = java_dialog('confirm', ...
1796 ['<HTML>Plugin <B>' PlugName '</B>: ' strUpdate '<BR>' ...
1797 'Download and install the latest version?<BR><BR>' strCompare], 'Plugin manager');
1798 % If update not confirmed: simply load the existing plugin
1799 if ~isConfirm
1800 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1801 return;
1802 end
1803 end
1804 disp(['BST> Plugin ' PlugName ' is outdated and will be updated.']);
1805 % Uninstall existing plugin
1806 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
1807 if ~isOk
1808 errMsg = ['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10];
1809 return;
1810 end
1811
1812 % No update: Load existing plugin and return
1813 else
1814 % Load plugin
1815 if ~OldPlugDesc.isLoaded
1816 [isLoaded, errMsg, PlugDesc] = Load(OldPlugDesc);
1817 if ~isLoaded
1818 errMsg = ['Could not load plugin ' PlugName ':' 10 errMsg];
1819 return;
1820 end
1821 else
1822 disp(['BST> Plugin ' PlugName ' already loaded: ' OldPlugDesc.Path]);
1823 end
1824 % Return old plugin
1825 PlugDesc = OldPlugDesc;
1826 isOk = 1;
1827 return;
1828 end
1829 else
1830 % Get user confirmation
1831 if isInteractive
1832 if ~isempty(PlugDesc.Version) && ~isequal(PlugDesc.Version, 'github-master') && ~isequal(PlugDesc.Version, 'latest')
1833 strVer = ['<FONT color="#707070">Latest version: ' PlugDesc.Version '</FONT><BR><BR>'];
1834 else
1835 strVer = '';
1836 end
1837 isConfirm = java_dialog('confirm', ...
1838 ['<HTML>Plugin <B>' PlugName '</B> is not installed on your computer.<BR>' ...
1839 '<B>Download</B> the latest version of ' PlugName ' now?<BR><BR>' ...
1840 strVer, ...
1841 '<FONT color="#707070">If this program is available on your computer,<BR>' ...
1842 'cancel this installation and use the menu: Plugins > <BR>' ...
1843 PlugName ' > Custom install > Set installation folder.</FONT><BR><BR>'], 'Plugin manager');
1844 if ~isConfirm
1845 errMsg = 'Installation aborted by user.';
1846 return;
1847 end
1848 end
1849 end
1850
1851 % === INSTALL PLUGIN ===
1852 bst_progress('text', ['Installing plugin ' PlugName '...']);
1853 % Managed plugin folder
1854 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
1855 % Delete existing folder
1856 if isdir(PlugPath)
1857 file_delete(PlugPath, 1, 3);
1858 end
1859 % Create folder
1860 if ~isdir(PlugPath)
1861 res = mkdir(PlugPath);
1862 if ~res
1863 errMsg = ['Error: Cannot create folder' 10 PlugPath];
1864 return
1865 end
1866 end
1867 % Setting progressbar image
1868 LogoFile = GetLogoFile(PlugDesc);
1869 if ~isempty(LogoFile)
1870 bst_progress('setimage', LogoFile);
1871 end
1872 % Get package file format
1873 if strcmpi(PlugDesc.URLzip(end-3:end), '.zip')
1874 pkgFormat = 'zip';
1875 elseif strcmpi(PlugDesc.URLzip(end-6:end), '.tar.gz') || strcmpi(PlugDesc.URLzip(end-3:end), '.tgz')
1876 pkgFormat = 'tgz';
1877 else
1878 disp('BST> Could not guess file format, trying ZIP...');
1879 pkgFormat = 'zip';
1880 end
1881 % Download file
1882 pkgFile = bst_fullfile(PlugPath, ['plugin.' pkgFormat]);
1883 disp(['BST> Downloading URL : ' PlugDesc.URLzip]);
1884 disp(['BST> Saving to file : ' pkgFile]);
1885 errMsg = gui_brainstorm('DownloadFile', PlugDesc.URLzip, pkgFile, ['Download plugin: ' PlugName], LogoFile);
1886 % If file was not downloaded correctly
1887 if ~isempty(errMsg)
1888 errMsg = ['Impossible to download ' PlugName ' automatically:' 10 errMsg];
1889 if ~isCompiled
1890 errMsg = [errMsg 10 10 ...
1891 'Alternative download solution:' 10 ...
1892 '1) Copy the URL below from the Matlab command window: ' 10 ...
1893 ' ' PlugDesc.URLzip 10 ...
1894 '2) Paste it in a web browser' 10 ...
1895 '3) Save the file and unzip it' 10 ...
1896 '4) Add to the Matlab path the folder containing ' PlugDesc.TestFile '.'];
1897 end
1898 bst_progress('removeimage');
1899 return;
1900 end
1901 % Update progress bar
1902 bst_progress('text', ['Installing plugin: ' PlugName '...']);
1903 if ~isempty(LogoFile)
1904 bst_progress('setimage', LogoFile);
1905 end
1906 % Unzip file
1907 switch (pkgFormat)
1908 case 'zip'
1909 bst_unzip(pkgFile, PlugPath);
1910 case 'tgz'
1911 if ispc
1912 untar(pkgFile, PlugPath);
1913 else
1914 curdir = pwd;
1915 cd(PlugPath);
1916 system(['tar -xf ' pkgFile]);
1917 cd(curdir);
1918 end
1919 end
1920 file_delete(pkgFile, 1, 3);
1921
1922 % === SAVE PLUGIN.MAT ===
1923 PlugDesc.Path = PlugPath;
1924 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
1925 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
1926 PlugDescSave = rmfield(PlugDesc, excludedFields);
1927 bst_save(PlugMatFile, PlugDescSave, 'v6');
1928
1929 % === CALLBACK: POST-DOWNLOADED ===
1930 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'DownloadedFcn');
1931 if ~isOk
1932 return;
1933 end
1934
1935 % === LOAD PLUGIN ===
1936 % Load plugin
1937 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1938 if ~isOk
1939 bst_progress('removeimage');
1940 return;
1941 end
1942 % Update plugin description after first load, and delete unwanted files
1943 [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, 1);
1944 if ~isOk
1945 return;
1946 end
1947
1948 % === SHOW PLUGIN INFO ===
1949 % Log install
1950 bst_webread(['https://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=' PlugDesc.Name '&action=install']);
1951 % Show plugin information (interactive mode only)
1952 if isInteractive
1953 % Hide progress bar
1954 isProgress = bst_progress('isVisible');
1955 if isProgress
1956 bst_progress('hide');
1957 end
1958 % Message box: aknowledgements
1959 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> was sucessfully installed.<BR><BR>' ...
1960 'This software is not distributed by the Brainstorm developers.<BR>' ...
1961 'Please take a few minutes to read the license information,<BR>' ...
1962 'check the authors'' website and register online if recommended.<BR><BR>' ...
1963 '<B>Cite the authors</B> in your publications if you are using their software.<BR><BR>'], 'Plugin manager');
1964 % Show the readme file
1965 if ~isempty(PlugDesc.ReadmeFile)
1966 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
1967 end
1968 % Open the website
1969 if ~isempty(PlugDesc.URLinfo)
1970 web(PlugDesc.URLinfo, '-browser')
1971 end
1972 % Restore progress bar
1973 if isProgress
1974 bst_progress('show');
1975 end
1976 end
1977 % Remove logo
1978 bst_progress('removeimage');
1979 % Return success
1980 isOk = 1;
1981 % Restore calling progress bar
1982 if isCallBar
1983 bst_progress('setbarparams', pBarParams);
1984 end
1985 end
1986
1987
1988 %% ===== UPDATE DESCRIPTION =====
1989 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('UpdateDescription', PlugDesc, doDelete=0)
1990 function [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, doDelete)
1991 isOk = 1;
1992 errMsg = '';
1993 PlugPath = PlugDesc.Path;
1994 PlugName = PlugDesc.Name;
1995
1996 if nargin < 2
1997 doDelete = 0;
1998 end
1999
2000 % Plug in needs to be installed
2001 if isempty(bst_plugin('GetInstalled', PlugDesc.Name))
2002 isOk = 0;
2003 errMsg = ['Cannot update description, plugin ''' PlugDesc.Name ''' needs to be installed'];
2004 return
2005 end
2006
2007 % === DELETE UNWANTED FILES ===
2008 if doDelete && ~isempty(PlugDesc.DeleteFiles) && iscell(PlugDesc.DeleteFiles)
2009 warning('off', 'MATLAB:RMDIR:RemovedFromPath');
2010 for iDel = 1:length(PlugDesc.DeleteFiles)
2011 if ~isempty(PlugDesc.SubFolder)
2012 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.DeleteFiles{iDel});
2013 else
2014 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.DeleteFiles{iDel});
2015 end
2016 if file_exist(fileDel)
2017 try
2018 file_delete(fileDel, 1, 3);
2019 catch
2020 disp(['BST> Plugin ' PlugName ': Could not delete file: ' PlugDesc.DeleteFiles{iDel}]);
2021 end
2022 else
2023 disp(['BST> Plugin ' PlugName ': Missing file: ' PlugDesc.DeleteFiles{iDel}]);
2024 end
2025 end
2026 warning('on', 'MATLAB:RMDIR:RemovedFromPath');
2027 end
2028
2029 % === SEARCH PROCESSES ===
2030 % Look for process_* functions in the process folder
2031 PlugProc = file_find(PlugPath, 'process_*.m', Inf, 0);
2032 if ~isempty(PlugProc)
2033 % Remove absolute path: use only path relative to the plugin Path
2034 PlugDesc.Processes = cellfun(@(c)file_win2unix(strrep(c, [PlugPath, filesep], '')), PlugProc, 'UniformOutput', 0);
2035 end
2036
2037 % === SAVE PLUGIN.MAT ===
2038 % Save installation date
2039 c = clock();
2040 PlugDesc.InstallDate = datestr(datenum(c(1), c(2), c(3), c(4), c(5), c(6)), 'dd-mmm-yyyy HH:MM:SS');
2041 % Get readme and logo
2042 PlugDesc.ReadmeFile = GetReadmeFile(PlugDesc);
2043 PlugDesc.LogoFile = GetLogoFile(PlugDesc);
2044 % Update plugin.mat
2045 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
2046 PlugDescSave = rmfield(PlugDesc, excludedFields);
2047 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
2048 bst_save(PlugMatFile, PlugDescSave, 'v6');
2049
2050 % === CALLBACK: POST-INSTALL ===
2051 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'InstalledFcn');
2052 if ~isOk
2053 return;
2054 end
2055
2056 % === GET INSTALLED VERSION ===
2057 % Get installed version
2058 if ~isempty(PlugDesc.GetVersionFcn)
2059 testVer = [];
2060 try
2061 if ischar(PlugDesc.GetVersionFcn)
2062 testVer = eval(PlugDesc.GetVersionFcn);
2063 elseif isa(PlugDesc.GetVersionFcn, 'function_handle')
2064 testVer = feval(PlugDesc.GetVersionFcn);
2065 end
2066 catch
2067 disp(['BST> Could not get installed version with callback: ' PlugDesc.GetVersionFcn]);
2068 end
2069 if ~isempty(testVer)
2070 PlugDesc.Version = testVer;
2071 % Update plugin.mat
2072 PlugDescSave.Version = testVer;
2073 bst_save(PlugMatFile, PlugDescSave, 'v6');
2074 end
2075 end
2076 end
2077
2078 %% ===== INSTALL INTERACTIVE =====
2079 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
2080 function [isOk, errMsg, PlugDesc] = InstallInteractive(PlugName)
2081 % Open progress bar
2082 isProgress = bst_progress('isVisible');
2083 if ~isProgress
2084 bst_progress('start', 'Plugin manager', 'Initialization...');
2085 end
2086 % Call silent function
2087 [isOk, errMsg, PlugDesc] = Install(PlugName, 1);
2088 % Handle errors
2089 if ~isOk
2090 bst_error(['Installation error:' 10 10 errMsg 10], 'Plugin manager', 0);
2091 elseif ~isempty(errMsg)
2092 java_dialog('msgbox', ['Installation message:' 10 10 errMsg 10], 'Plugin manager');
2093 end
2094 % Close progress bar
2095 if ~isProgress
2096 bst_progress('stop');
2097 end
2098 end
2099
2100
2101 %% ===== INSTALL MULTIPLE CHOICE =====
2102 % If multiple plugins provide the same functions (eg. FieldTrip and SPM): make sure at least one is installed
2103 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice', PlugNames, isInteractive)
2104 function [isOk, errMsg, PlugDesc] = InstallMultipleChoice(PlugNames, isInteractive)
2105 if (nargin < 2) || isempty(isInteractive)
2106 isInteractive = 0;
2107 end
2108 % Check if one of the plugins is loaded
2109 for iPlug = 1:length(PlugNames)
2110 PlugInst = GetInstalled(PlugNames{iPlug});
2111 if ~isempty(PlugInst)
2112 [isOk, errMsg, PlugDesc] = Load(PlugNames{iPlug});
2113 if isOk
2114 return;
2115 end
2116 end
2117 end
2118 % If no plugin is loaded: Install the first in the list
2119 [isOk, errMsg, PlugDesc] = Install(PlugNames{1}, isInteractive);
2120 end
2121
2122
2123 %% ===== UNINSTALL =====
2124 % USAGE: [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
2125 function [isOk, errMsg] = Uninstall(PlugName, isInteractive, isDependencies)
2126 % Returned variables
2127 isOk = 0;
2128 errMsg = '';
2129 % Parse inputs
2130 if (nargin < 3) || isempty(isDependencies)
2131 isDependencies = 1;
2132 end
2133 if (nargin < 2) || isempty(isInteractive)
2134 isInteractive = 0;
2135 end
2136 if ~ischar(PlugName)
2137 errMsg = 'Invalid call to Uninstall()';
2138 return;
2139 end
2140
2141 % === CHECK INSTALLATION ===
2142 % Get installation
2143 PlugDesc = GetInstalled(PlugName);
2144 % External plugin
2145 if ~isempty(PlugDesc) && ~isequal(PlugDesc.isManaged, 1)
2146 errMsg = ['<HTML>Plugin <B>' PlugName '</B> is not managed by Brainstorm.' 10 'Delete folder manually:' 10 PlugDesc.Path];
2147 return;
2148 % Plugin not installed: check if folder exists
2149 elseif isempty(PlugDesc) || isempty(PlugDesc.Path)
2150 % Get plugin structure from name
2151 [PlugDesc, errMsg] = GetDescription(PlugName);
2152 if ~isempty(errMsg)
2153 return;
2154 end
2155 % Managed plugin folder
2156 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
2157 else
2158 PlugPath = PlugDesc.Path;
2159 end
2160 % Plugin not installed
2161 if ~file_exist(PlugPath)
2162 errMsg = ['Plugin ' PlugName ' is not installed.'];
2163 return;
2164 end
2165
2166 % === USER CONFIRMATION ===
2167 if isInteractive
2168 isConfirm = java_dialog('confirm', ['<HTML>Delete permanently plugin <B>' PlugName '</B>?' 10 10 PlugPath 10 10], 'Plugin manager');
2169 if ~isConfirm
2170 errMsg = 'Uninstall aborted by user.';
2171 return;
2172 end
2173 end
2174
2175 % === PROCESS DEPENDENCIES ===
2176 % Uninstall dependent plugins
2177 if isDependencies
2178 AllPlugs = GetSupported();
2179 for iPlug = 1:length(AllPlugs)
2180 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2181 disp(['BST> Uninstalling dependent plugin: ' AllPlugs(iPlug).Name]);
2182 Uninstall(AllPlugs(iPlug).Name, isInteractive);
2183 end
2184 end
2185 end
2186
2187 % === UNLOAD ===
2188 if isequal(PlugDesc.isLoaded, 1)
2189 [isUnloaded, errMsgUnload] = Unload(PlugDesc);
2190 if ~isempty(errMsgUnload)
2191 disp(['BST> Error unloading plugin ' PlugName ': ' errMsgUnload]);
2192 end
2193 end
2194
2195 % === UNINSTALL ===
2196 disp(['BST> Deleting plugin ' PlugName ': ' PlugPath]);
2197 % Delete plugin folder
2198 isDeleted = file_delete(PlugPath, 1, 3);
2199 if (isDeleted ~= 1)
2200 errMsg = ['Could not delete plugin folder: ' 10 PlugPath 10 10 ...
2201 'There is probably a file in that folder that is currently ' 10 ...
2202 'loaded in Matlab, but that cannot be unloaded dynamically.' 10 10 ...
2203 'Brainstorm will now close Matlab.' 10 ...
2204 'Restart Matlab and install again the plugin.' 10 10];
2205 if isInteractive
2206 java_dialog('error', errMsg, 'Restart Matlab');
2207 else
2208 disp([10 10 'BST> ' errMsg]);
2209 end
2210 quit('force');
2211 end
2212
2213 % === CALLBACK: POST-UNINSTALL ===
2214 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UninstalledFcn');
2215 if ~isOk
2216 return;
2217 end
2218
2219 % Return success
2220 isOk = 1;
2221 end
2222
2223
2224 %% ===== UNINSTALL INTERACTIVE =====
2225 % USAGE: [isOk, errMsg] = bst_plugin('UninstallInteractive', PlugName)
2226 function [isOk, errMsg] = UninstallInteractive(PlugName)
2227 % Open progress bar
2228 isProgress = bst_progress('isVisible');
2229 if ~isProgress
2230 bst_progress('start', 'Plugin manager', 'Initialization...');
2231 end
2232 % Call silent function
2233 [isOk, errMsg] = Uninstall(PlugName, 1);
2234 % Handle errors
2235 if ~isOk
2236 bst_error(['An error occurred while uninstalling plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2237 elseif ~isempty(errMsg)
2238 java_dialog('msgbox', ['Uninstall message:' 10 10 errMsg 10], 'Plugin manager');
2239 end
2240 % Close progress bar
2241 if ~isProgress
2242 bst_progress('stop');
2243 end
2244 end
2245
2246
2247 %% ===== UPDATE INTERACTIVE =====
2248 % USAGE: [isOk, errMsg] = bst_plugin('UpdateInteractive', PlugName)
2249 function [isOk, errMsg] = UpdateInteractive(PlugName)
2250 % Open progress bar
2251 isProgress = bst_progress('isVisible');
2252 if ~isProgress
2253 bst_progress('start', 'Plugin manager', 'Initialization...');
2254 end
2255 % Get new plugin
2256 [PlugRef, errMsg] = GetDescription(PlugName);
2257 isOk = isempty(errMsg);
2258 % Get installed plugin
2259 if isOk
2260 PlugInst = GetInstalled(PlugName);
2261 if isempty(PlugInst) || ~PlugInst.isManaged
2262 isOk = 0;
2263 errMsg = ['Plugin ' PlugName ' is not installed or not managed by Brainstorm.'];
2264 end
2265 end
2266 % Get online update (use cache when available)
2267 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugRef.URLzip, 1);
2268 if ~isempty(newVersion)
2269 PlugRef.Version = newVersion;
2270 end
2271 if ~isempty(newURLzip)
2272 PlugRef.URLzip = newURLzip;
2273 end
2274 % User confirmation
2275 if isOk
2276 isOk = java_dialog('confirm', ['<HTML>Update plugin <B>' PlugName '</B> ?<BR><BR><FONT color="#707070">' ...
2277 'Old version : <I>' PlugInst.Version '</I><BR>' ...
2278 'New version : <I>' PlugRef.Version '</I><BR><BR></FONT>'], 'Plugin manager');
2279 if ~isOk
2280 errMsg = 'Update aborted by user.';
2281 end
2282 end
2283 % Uninstall old
2284 if isOk
2285 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
2286 end
2287 % Install new
2288 if isOk
2289 [isOk, errMsg, PlugDesc] = Install(PlugName, 0);
2290 else
2291 PlugDesc = [];
2292 end
2293 % Handle errors
2294 if ~isOk
2295 bst_error(['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2296 elseif ~isempty(errMsg)
2297 java_dialog('msgbox', ['Update message:' 10 10 errMsg 10], 'Plugin manager');
2298 end
2299 % Close progress bar
2300 if ~isProgress
2301 bst_progress('stop');
2302 end
2303 % Plugin was updated successfully
2304 if ~isempty(PlugDesc)
2305 % Show the readme file
2306 if ~isempty(PlugDesc.ReadmeFile)
2307 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
2308 end
2309 % Open the website
2310 if ~isempty(PlugDesc.URLinfo)
2311 web(PlugDesc.URLinfo, '-browser')
2312 end
2313 end
2314 end
2315
2316
2317 %% ===== LOAD =====
2318 % USAGE: [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose=1)
2319 function [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose)
2320 % Parse inputs
2321 if (nargin < 2) || isempty(isVerbose)
2322 isVerbose = 1;
2323 end
2324 % Initialize returned variables
2325 isOk = 0;
2326 % Get plugin structure from name
2327 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2328 if ~isempty(errMsg)
2329 return;
2330 end
2331 % Check if plugin is supported on Apple silicon
2332 OsType = bst_get('OsType', 0);
2333 if strcmpi(OsType, 'mac64arm') && ismember(PlugDesc.Name, PluginsNotSupportAppleSilicon())
2334 errMsg = ['Plugin ', PlugDesc.Name ' is not supported on Apple silicon yet.'];
2335 return;
2336 end
2337 % Minimum Matlab version
2338 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
2339 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
2340 errMsg = ['Plugin ', PlugDesc.Name ' is not supported for versions of Matlab <= ' strMinVer];
2341 return;
2342 end
2343
2344 % === PROCESS DEPENDENCIES ===
2345 % Unload incompatible plugins
2346 if ~isempty(PlugDesc.UnloadPlugs)
2347 for iPlug = 1:length(PlugDesc.UnloadPlugs)
2348 % disp(['BST> Unloading incompatible plugin: ' PlugDesc.UnloadPlugs{iPlug}]);
2349 Unload(PlugDesc.UnloadPlugs{iPlug}, isVerbose);
2350 end
2351 end
2352
2353 % === ALREADY LOADED ===
2354 % If plugin is already full loaded
2355 if isequal(PlugDesc.isLoaded, 1) && ~isempty(PlugDesc.Path)
2356 if isVerbose
2357 errMsg = ['Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path];
2358 end
2359 return;
2360 end
2361 % Managed plugin path
2362 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2363 if file_exist(PlugPath)
2364 PlugDesc.isManaged = 1;
2365 % Custom installation
2366 else
2367 PluginCustomPath = bst_get('PluginCustomPath');
2368 if isfield(PluginCustomPath, PlugDesc.Name) && ~isempty(bst_fullfile(PluginCustomPath.(PlugDesc.Name))) && file_exist(bst_fullfile(PluginCustomPath.(PlugDesc.Name)))
2369 PlugPath = PluginCustomPath.(PlugDesc.Name);
2370 end
2371 PlugDesc.isManaged = 0;
2372 end
2373 % Managed install: Detect if there is a single subfolder containing all the files
2374 if PlugDesc.isManaged && ~isempty(PlugDesc.TestFile) && ~file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
2375 dirList = dir(PlugPath);
2376 for iDir = 1:length(dirList)
2377 % Not folder or . : skip
2378 if (dirList(iDir).name(1) == '.') || ~dirList(iDir).isdir
2379 continue;
2380 end
2381 % Check if test file is in the folder
2382 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.TestFile))
2383 PlugDesc.SubFolder = dirList(iDir).name;
2384 break;
2385 % Otherwise, check in any of the subfolders
2386 elseif ~isempty(PlugDesc.LoadFolders)
2387 % All subfolders
2388 if isequal(PlugDesc.LoadFolders, '*') || isequal(PlugDesc.LoadFolders, {'*'})
2389 if ~isempty(file_find(bst_fullfile(PlugPath, dirList(iDir).name), PlugDesc.TestFile))
2390 PlugDesc.SubFolder = dirList(iDir).name;
2391 break;
2392 end
2393 % Specific subfolders
2394 else
2395 for iSubDir = 1:length(PlugDesc.LoadFolders)
2396 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.LoadFolders{iSubDir}, PlugDesc.TestFile))
2397 PlugDesc.SubFolder = dirList(iDir).name;
2398 break;
2399 end
2400 end
2401 end
2402 end
2403 end
2404 end
2405 % Check if test function already available in the path
2406 TestFilePath = GetTestFilePath(PlugDesc);
2407 if ~isempty(TestFilePath)
2408 PlugDesc.isLoaded = 1;
2409 % Handle case symbolic link
2410 try
2411 PlugPath = builtin('_canonicalizepath', PlugPath);
2412 catch
2413 % Nothing here
2414 end
2415 PlugDesc.isManaged = strMatchEdge(which(PlugDesc.TestFile), PlugPath, 'start');
2416 if PlugDesc.isManaged
2417 PlugDesc.Path = PlugPath;
2418 else
2419 PlugDesc.Path = TestFilePath;
2420 end
2421 if isVerbose
2422 disp(['BST> Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path]);
2423 end
2424 isOk = 1;
2425 return;
2426 end
2427
2428 % === CHECK LOADABILITY ===
2429 PlugDesc.Path = PlugPath;
2430 if ~file_exist(PlugDesc.Path)
2431 errMsg = ['Plugin ' PlugDesc.Name ' not installed.' 10 'Missing folder: ' PlugDesc.Path];
2432 return;
2433 end
2434 % Set logo
2435 LogoFile = GetLogoFile(PlugDesc);
2436 if ~isempty(LogoFile)
2437 bst_progress('setimage', LogoFile);
2438 end
2439
2440 % Load required plugins
2441 if ~isempty(PlugDesc.RequiredPlugs)
2442 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
2443 % disp(['BST> Loading required plugin: ' PlugDesc.RequiredPlugs{iPlug,1}]);
2444 [isOk, errMsg] = Load(PlugDesc.RequiredPlugs{iPlug,1}, isVerbose);
2445 if ~isOk
2446 errMsg = ['Error processing dependencies: ', PlugDesc.Name, 10, errMsg];
2447 bst_progress('removeimage');
2448 return;
2449 end
2450 end
2451 end
2452
2453 % === LOAD PLUGIN ===
2454 % Add plugin folder to path
2455 if ~isempty(PlugDesc.SubFolder)
2456 PlugHomeDir = bst_fullfile(PlugPath, PlugDesc.SubFolder);
2457 else
2458 PlugHomeDir = PlugPath;
2459 end
2460 % Do not modify path in compiled mode
2461 isCompiled = bst_iscompiled();
2462 if ~isCompiled
2463 % Handle case symbolic link
2464 try
2465 PlugHomeDir = builtin('_canonicalizepath', PlugHomeDir);
2466 catch
2467 % Nothing here
2468 end
2469 addpath(PlugHomeDir);
2470 if isVerbose
2471 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ' PlugHomeDir]);
2472 end
2473 % Add specific subfolders to path
2474 if ~isempty(PlugDesc.LoadFolders)
2475 % Load all all subfolders
2476 if isequal(PlugDesc.LoadFolders, '*') || isequal(PlugDesc.LoadFolders, {'*'})
2477 if isVerbose
2478 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, '*']);
2479 end
2480 addpath(genpath(PlugHomeDir));
2481 % Load specific subfolders
2482 else
2483 for i = 1:length(PlugDesc.LoadFolders)
2484 subDir = PlugDesc.LoadFolders{i};
2485 if isequal(filesep, '\')
2486 subDir = strrep(subDir, '/', '\');
2487 end
2488 if ~isempty(dir([PlugHomeDir, filesep, subDir]))
2489 if isVerbose
2490 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, subDir]);
2491 end
2492 if regexp(subDir, '\*[/\\]*$')
2493 subDir = regexprep(subDir, '\*[/\\]*$', '');
2494 addpath(genpath([PlugHomeDir, filesep, subDir]));
2495 else
2496 addpath([PlugHomeDir, filesep, subDir]);
2497 end
2498 end
2499 end
2500 end
2501 end
2502 end
2503
2504 % === TEST FUNCTION ===
2505 % Check if test function is available on path
2506 TestFilePath = GetTestFilePath(PlugDesc);
2507 if ~isCompiled && ~isempty(PlugDesc.TestFile) && (exist(TestFilePath, 'file') == 0)
2508 errMsg = ['Plugin ' PlugDesc.Name ' successfully loaded from:' 10 PlugHomeDir 10 10 ...
2509 'However, the function ' PlugDesc.TestFile ' is not accessible in the Matlab path.' 10 10 ...
2510 'Try the following:' 10 ...
2511 '1. Update the plugin ' PlugDesc.Name 10 ...
2512 '2. If the issue persists, restart Matlab and Brainstorm.'];
2513 bst_progress('removeimage');
2514 return;
2515 end
2516
2517 % === CALLBACK: POST-LOAD ===
2518 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'LoadedFcn');
2519
2520 % Remove logo
2521 bst_progress('removeimage');
2522 % Return success
2523 PlugDesc.isLoaded = isOk;
2524 end
2525
2526
2527 %% ===== LOAD INTERACTIVE =====
2528 % USAGE: [isOk, errMsg, PlugDesc] = LoadInteractive(PlugName/PlugDesc)
2529 function [isOk, errMsg, PlugDesc] = LoadInteractive(PlugDesc)
2530 % Open progress bar
2531 isProgress = bst_progress('isVisible');
2532 if ~isProgress
2533 bst_progress('start', 'Plugin manager', 'Loading plugin...');
2534 end
2535 % Call silent function
2536 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
2537 % Handle errors
2538 if ~isOk
2539 bst_error(['Load error:' 10 10 errMsg 10], 'Plugin manager', 0);
2540 elseif ~isempty(errMsg)
2541 java_dialog('msgbox', ['Load message:' 10 10 errMsg 10], 'Plugin manager');
2542 end
2543 % Close progress bar
2544 if ~isProgress
2545 bst_progress('stop');
2546 end
2547 end
2548
2549
2550 %% ===== UNLOAD =====
2551 % USAGE: [isOk, errMsg, PlugDesc] = Unload(PlugName/PlugDesc, isVerbose)
2552 function [isOk, errMsg, PlugDesc] = Unload(PlugDesc, isVerbose)
2553 % Parse inputs
2554 if (nargin < 2) || isempty(isVerbose)
2555 isVerbose = 1;
2556 end
2557 % Initialize returned variables
2558 isOk = 0;
2559 errMsg = '';
2560 % Get installation
2561 InstPlugDesc = GetInstalled(PlugDesc);
2562 % Plugin not installed: check if folder exists
2563 if isempty(InstPlugDesc) || isempty(InstPlugDesc.Path)
2564 % Get plugin structure from name
2565 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2566 if ~isempty(errMsg)
2567 return;
2568 end
2569 % Managed plugin folder
2570 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2571 else
2572 PlugDesc = InstPlugDesc;
2573 PlugPath = PlugDesc.Path;
2574 end
2575 % Plugin not installed
2576 if ~file_exist(PlugPath)
2577 errMsg = ['Plugin ' PlugDesc.Name ' is not installed.' 10 'Missing folder: ' PlugPath];
2578 return;
2579 end
2580 % Get plugin structure from name
2581 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2582 if ~isempty(errMsg)
2583 return;
2584 end
2585
2586 % === PROCESS DEPENDENCIES ===
2587 % Unload dependent plugins
2588 AllPlugs = GetSupported();
2589 for iPlug = 1:length(AllPlugs)
2590 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2591 Unload(AllPlugs(iPlug));
2592 end
2593 end
2594
2595 % === UNLOAD PLUGIN ===
2596 % Do not modify path in compiled mode
2597 if ~bst_iscompiled()
2598 matlabPath = str_split(path, pathsep);
2599 % Remove plugin folder and subfolders from path
2600 allSubFolders = str_split(genpath(PlugPath), pathsep);
2601 for i = 1:length(allSubFolders)
2602 if ismember(allSubFolders{i}, matlabPath)
2603 rmpath(allSubFolders{i});
2604 if isVerbose
2605 disp(['BST> Removing plugin ' PlugDesc.Name ' from path: ' allSubFolders{i}]);
2606 end
2607 end
2608 end
2609 end
2610
2611 % === TEST FUNCTION ===
2612 % Check if test function is still available on path
2613 TestFilePath = GetTestFilePath(PlugDesc);
2614 if ~isempty(PlugDesc.TestFile) && ~isempty(TestFilePath)
2615 errMsg = ['Plugin ' PlugDesc.Name ' successfully unloaded from: ' 10 PlugPath 10 10 ...
2616 'However, another version is still accessible on the Matlab path:' 10 which(PlugDesc.TestFile) 10 10 ...
2617 'Please remove this folder from the Matlab path.'];
2618 return;
2619 end
2620
2621 % === CALLBACK: POST-UNLOAD ===
2622 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UnloadedFcn');
2623 if ~isOk
2624 return;
2625 end
2626
2627 % Return success
2628 PlugDesc.isLoaded = 0;
2629 isOk = 1;
2630 end
2631
2632
2633 %% ===== UNLOAD INTERACTIVE =====
2634 % USAGE: [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugName/PlugDesc)
2635 function [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugDesc)
2636 % Open progress bar
2637 isProgress = bst_progress('isVisible');
2638 if ~isProgress
2639 bst_progress('start', 'Plugin manager', 'Unloading plugin...');
2640 end
2641 % Call silent function
2642 [isOk, errMsg, PlugDesc] = Unload(PlugDesc);
2643 % Handle errors
2644 if ~isOk
2645 bst_error(['Unload error:' 10 10 errMsg 10], 'Plugin manager', 0);
2646 elseif ~isempty(errMsg)
2647 java_dialog('msgbox', ['Unload message:' 10 10 errMsg 10], 'Plugin manager');
2648 end
2649 % Close progress bar
2650 if ~isProgress
2651 bst_progress('stop');
2652 end
2653 end
2654
2655
2656 %% ===== ENSURE =====
2657 % USAGE: [ensureResult, errMsg, PlugDesc] = bst_plugin('Ensure', PlugName/PlugDesc, isInteractive, getLatestVersion)
2658 function [ensureResult, errMsg, PlugDesc] = Ensure(PlugDesc, isInteractive, getLatestVersion)
2659 % Parse inputs
2660 if (nargin < 2) || isempty(isInteractive)
2661 isInteractive = 0;
2662 end
2663 if (nargin < 3) || isempty(getLatestVersion)
2664 getLatestVersion = 0;
2665 end
2666 % Initialize returned variables
2667 ensureResult = [];
2668 % Get plugin structure from name
2669 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2670 if ~isempty(errMsg)
2671 return
2672 end
2673 % Ensure pluging is available
2674 InstalledPlugDesc = GetInstalled(PlugDesc);
2675 % Install if not present or Update is required
2676 if isempty(InstalledPlugDesc) || InstalledPlugDesc.AutoUpdate || getLatestVersion
2677 % Install and load plugin
2678 [isOk, errMsg, PlugDesc] = Install(PlugDesc.Name, isInteractive);
2679 if ~isOk
2680 return
2681 end
2682 ensureResult = 1;
2683 % Plugin was already installed and loaded, keep like that even if it was updated
2684 if ~isempty(InstalledPlugDesc) && InstalledPlugDesc.isLoaded
2685 ensureResult = 0;
2686 end
2687 elseif ~InstalledPlugDesc.isLoaded
2688 % Load plugin
2689 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
2690 if ~isOk
2691 return
2692 end
2693 ensureResult = 2;
2694 else
2695 % Plugin is already installed and loaded, it was not updated
2696 ensureResult = 0;
2697 end
2698 end
2699
2700
2701 %% ===== LIST =====
2702 % USAGE: strList = bst_plugin('List', Target='installed', isGui=0) % Target={'supported','installed', 'loaded'}
2703 function strList = List(Target, isGui)
2704 % Parse inputs
2705 if (nargin < 2) || isempty(isGui)
2706 isGui = 0;
2707 end
2708 if (nargin < 1) || isempty(Target)
2709 Target = 'Installed';
2710 else
2711 Target = [upper(Target(1)), lower(Target(2:end))];
2712 end
2713 % Get plugins to list
2714 strTitle = sprintf('%s plugins', Target);
2715 switch (Target)
2716 case 'Supported'
2717 PlugDesc = GetSupported();
2718 isInstalled = 0;
2719 case 'Installed'
2720 strTitle = [strTitle ' (*=Loaded)'];
2721 PlugDesc = GetInstalled();
2722 isInstalled = 1;
2723 case 'Loaded'
2724 PlugDesc = GetLoaded();
2725 isInstalled = 1;
2726 otherwise
2727 error(['Invalid target: ' Target]);
2728 end
2729 if isempty(PlugDesc)
2730 return;
2731 end
2732 % Sort by plugin names
2733 [tmp,I] = sort({PlugDesc.Name});
2734 PlugDesc = PlugDesc(I);
2735
2736 % Get Brainstorm info
2737 bstVer = bst_get('Version');
2738 bstDir = bst_get('BrainstormHomeDir');
2739 % Cut version string (short github SHA)
2740 if (length(bstVer.Commit) > 13)
2741 bstGit = ['git @', bstVer.Commit(1:7)];
2742 bstURL = ['https://github.com/brainstorm-tools/brainstorm3/archive/' bstVer.Commit '.zip'];
2743 structVer = bstGit;
2744 else
2745 bstGit = '';
2746 bstURL = '';
2747 structVer = bstVer.Version;
2748 end
2749
2750 % Max lengths
2751 headerName = ' Name';
2752 headerVersion = 'Version';
2753 headerPath = 'Install path';
2754 headerUrl = 'Downloaded from';
2755 headerDate = 'Install date';
2756 maxName = max(cellfun(@length, {PlugDesc.Name, headerName, 'brainstorm'}));
2757 maxVer = min(13, max(cellfun(@length, {PlugDesc.Version, headerVersion, bstGit})));
2758 maxUrl = max(cellfun(@length, {PlugDesc.URLzip, headerUrl, bstURL}));
2759 maxDate = 12;
2760 if isInstalled
2761 strDate = [' | ', headerDate, repmat(' ', 1, maxDate-length(headerDate))];
2762 strDateSep = ['-|-', repmat('-',1,maxDate)];
2763 maxPath = max(cellfun(@length, {PlugDesc.Path, headerPath}));
2764 strPath = [' | ', headerPath, repmat(' ', 1, maxPath-length(headerPath))];
2765 strPathSep = ['-|-', repmat('-',1,maxPath)];
2766 strBstVer = [' | ', bstVer.Date, repmat(' ', 1, maxDate-length(bstVer.Date))];
2767 strBstDir = [' | ', bstDir, repmat(' ', 1, maxPath-length(bstDir))];
2768 else
2769 strDate = '';
2770 strDateSep = '';
2771 strPath = '';
2772 strPathSep = '';
2773 strBstVer = '';
2774 strBstDir = '';
2775 end
2776 % Print column headers
2777 strList = [headerName, repmat(' ', 1, maxName-length(headerName) + 2) ...
2778 ' | ', headerVersion, repmat(' ', 1, maxVer-length(headerVersion)), ...
2779 strDate, strPath, ...
2780 ' | ' headerUrl 10 ...
2781 repmat('-',1,maxName + 2), '-|-', repmat('-',1,maxVer), strDateSep, strPathSep, '-|-', repmat('-',1,maxUrl) 10];
2782
2783 % Print Brainstorm information
2784 strList = [strList '* ', ...
2785 'brainstorm', repmat(' ', 1, maxName-length('brainstorm')) ...
2786 ' | ', bstGit, repmat(' ', 1, maxVer-length(bstGit)), ...
2787 strBstVer, strBstDir, ...
2788 ' | ' bstURL 10];
2789
2790 % Print installed plugins to standard output
2791 for iPlug = 1:length(PlugDesc)
2792 % Loaded plugin
2793 if PlugDesc(iPlug).isLoaded
2794 strLoaded = '* ';
2795 else
2796 strLoaded = ' ';
2797 end
2798 % Cut installation date: Only date, no time
2799 if (length(PlugDesc(iPlug).InstallDate) > 11)
2800 plugDate = PlugDesc(iPlug).InstallDate(1:11);
2801 else
2802 plugDate = PlugDesc(iPlug).InstallDate;
2803 end
2804 % Installed listing
2805 if isInstalled
2806 strDate = [' | ', plugDate, repmat(' ', 1, maxDate-length(plugDate))];
2807 strPath = [' | ', PlugDesc(iPlug).Path, repmat(' ', 1, maxPath-length(PlugDesc(iPlug).Path))];
2808 else
2809 strDate = '';
2810 strPath = '';
2811 end
2812 % Get installed version
2813 if (length(PlugDesc(iPlug).Version) > 13) % Cut version string (short github SHA)
2814 plugVer = ['git @', PlugDesc(iPlug).Version(1:7)];
2815 else
2816 plugVer = PlugDesc(iPlug).Version;
2817 end
2818 % Get installed version with GetVersionFcn
2819 if isempty(plugVer) && isfield(PlugDesc(iPlug),'GetVersionFcn') && ~isempty(PlugDesc(iPlug).GetVersionFcn)
2820 % Load plugin if needed
2821 tmpLoad = 0;
2822 if ~PlugDesc(iPlug).isLoaded
2823 tmpLoad = 1;
2824 Load(PlugDesc(iPlug), 0);
2825 end
2826 try
2827 if ischar(PlugDesc(iPlug).GetVersionFcn)
2828 plugVer = eval(PlugDesc(iPlug).GetVersionFcn);
2829 elseif isa(PlugDesc(iPlug).GetVersionFcn, 'function_handle')
2830 plugVer = feval(PlugDesc(iPlug).GetVersionFcn);
2831 end
2832 catch
2833 disp(['BST> Could not get installed version with callback: ' PlugDesc(iPlug).GetVersionFcn]);
2834 end
2835 % Unload plugin
2836 if tmpLoad
2837 Unload(PlugDesc(iPlug), 0);
2838 end
2839 end
2840 % Assemble plugin text row
2841 strList = [strList strLoaded, ...
2842 PlugDesc(iPlug).Name, repmat(' ', 1, maxName-length(PlugDesc(iPlug).Name)) ...
2843 ' | ', plugVer, repmat(' ', 1, maxVer-length(plugVer)), ...
2844 strDate, strPath, ...
2845 ' | ' PlugDesc(iPlug).URLzip 10];
2846 end
2847 % Display output
2848 if isGui
2849 view_text(strList, strTitle);
2850 % No string returned: display it in the command window
2851 elseif (nargout == 0)
2852 disp([10 strTitle 10 10 strList]);
2853 end
2854 end
2855
2856
2857 %% ===== MENUS: CREATE =====
2858 function j = MenuCreate(jMenu, jPlugsPrev, PlugDesc, fontSize)
2859 import org.brainstorm.icon.*;
2860 % Get all the supported plugins
2861 if isempty(PlugDesc)
2862 PlugDesc = GetSupported();
2863 end
2864 % Get Matlab version
2865 MatlabVersion = bst_get('MatlabVersion');
2866 isCompiled = bst_iscompiled();
2867 % Submenus array
2868 jSub = {};
2869 % Generate submenus array from existing menu
2870 if ~isCompiled && jMenu.getMenuComponentCount > 0
2871 for iItem = 0 : jMenu.getItemCount-1
2872 if ~isempty(regexp(jMenu.getMenuComponent(iItem).class, 'JMenu$', 'once'))
2873 jSub(end+1,1:2) = {char(jMenu.getMenuComponent(iItem).getText), jMenu.getMenuComponent(iItem)};
2874 end
2875 end
2876 end
2877 % Editing an existing menu?
2878 if isempty(jPlugsPrev)
2879 isNewMenu = 1;
2880 j = repmat(struct(), 0);
2881 else
2882 isNewMenu = 0;
2883 j = repmat(jPlugsPrev(1), 0);
2884 end
2885 % Process each plugin
2886 for iPlug = 1:length(PlugDesc)
2887 Plug = PlugDesc(iPlug);
2888 % Skip if Matlab is too old
2889 if ~isempty(Plug.MinMatlabVer) && (Plug.MinMatlabVer > 0) && (MatlabVersion < Plug.MinMatlabVer)
2890 continue;
2891 end
2892 % Skip if not supported in compiled version
2893 if isCompiled && (Plug.CompiledStatus == 0)
2894 continue;
2895 end
2896 % === Add menus for each plugin ===
2897 % One menu per plugin
2898 ij = length(j) + 1;
2899 j(ij).name = Plug.Name;
2900 % Skip if it is already a menu item
2901 if ~isNewMenu
2902 iPlugPrev = ismember({jPlugsPrev.name}, Plug.Name);
2903 if any(iPlugPrev)
2904 j(ij) = jPlugsPrev(iPlugPrev);
2905 continue
2906 end
2907 end
2908 % Category=submenu
2909 if ~isempty(Plug.Category)
2910 if isempty(jSub) || ~ismember(Plug.Category, jSub(:,1))
2911 jParent = gui_component('Menu', jMenu, [], Plug.Category, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2912 jSub(end+1,1:2) = {Plug.Category, jParent};
2913 else
2914 iSub = find(strcmpi(jSub(:,1), Plug.Category));
2915 jParent = jSub{iSub,2};
2916 end
2917 else
2918 jParent = jMenu;
2919 end
2920 % Compiled and included: Simple static menu
2921 if isCompiled && (Plug.CompiledStatus == 2)
2922 j(ij).menu = gui_component('MenuItem', jParent, [], Plug.Name, [], [], [], fontSize);
2923 % Do not create submenus for compiled version
2924 else
2925 % Main menu
2926 j(ij).menu = gui_component('Menu', jParent, [], Plug.Name, [], [], [], fontSize);
2927 % Version
2928 j(ij).version = gui_component('MenuItem', j(ij).menu, [], 'Version', [], [], [], fontSize);
2929 j(ij).versep = java_create('javax.swing.JSeparator');
2930 j(ij).menu.add(j(ij).versep);
2931 % Install
2932 j(ij).install = gui_component('MenuItem', j(ij).menu, [], 'Install', IconLoader.ICON_DOWNLOAD, [], @(h,ev)InstallInteractive(Plug.Name), fontSize);
2933 % Update
2934 j(ij).update = gui_component('MenuItem', j(ij).menu, [], 'Update', IconLoader.ICON_RELOAD, [], @(h,ev)UpdateInteractive(Plug.Name), fontSize);
2935 % Uninstall
2936 j(ij).uninstall = gui_component('MenuItem', j(ij).menu, [], 'Uninstall', IconLoader.ICON_DELETE, [], @(h,ev)UninstallInteractive(Plug.Name), fontSize);
2937 j(ij).menu.addSeparator();
2938 % Custom install
2939 j(ij).custom = gui_component('Menu', j(ij).menu, [], 'Custom install', IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2940 j(ij).customset = gui_component('MenuItem', j(ij).custom, [], 'Select installation folder', [], [], @(h,ev)SetCustomPath(Plug.Name), fontSize);
2941 j(ij).custompath = gui_component('MenuItem', j(ij).custom, [], 'Path not set', [], [], [], fontSize);
2942 j(ij).custompath.setEnabled(0);
2943 j(ij).custom.addSeparator();
2944 j(ij).customdel = gui_component('MenuItem', j(ij).custom, [], 'Ignore local installation', [], [], @(h,ev)SetCustomPath(Plug.Name, 0), fontSize);
2945 j(ij).menu.addSeparator();
2946 % Load
2947 j(ij).load = gui_component('MenuItem', j(ij).menu, [], 'Load', IconLoader.ICON_GOOD, [], @(h,ev)LoadInteractive(Plug.Name), fontSize);
2948 j(ij).unload = gui_component('MenuItem', j(ij).menu, [], 'Unload', IconLoader.ICON_BAD, [], @(h,ev)UnloadInteractive(Plug.Name), fontSize);
2949 j(ij).menu.addSeparator();
2950 % Website
2951 j(ij).web = gui_component('MenuItem', j(ij).menu, [], 'Website', IconLoader.ICON_EXPLORER, [], @(h,ev)web(Plug.URLinfo, '-browser'), fontSize);
2952 j(ij).usage = gui_component('MenuItem', j(ij).menu, [], 'Usage statistics', IconLoader.ICON_TS_DISPLAY, [], @(h,ev)bst_userstat(0,Plug.Name), fontSize);
2953 % Extra menus
2954 if ~isempty(Plug.ExtraMenus)
2955 j(ij).menu.addSeparator();
2956 for iMenu = 1:size(Plug.ExtraMenus,1)
2957 j(ij).extra(iMenu) = gui_component('MenuItem', j(ij).menu, [], Plug.ExtraMenus{iMenu,1}, IconLoader.ICON_EXPLORER, [], @(h,ev)bst_call(@eval, Plug.ExtraMenus{iMenu,2}), fontSize);
2958 end
2959 end
2960 end
2961 end
2962 % === Remove menus for plugins with description ===
2963 if ~isempty(jPlugsPrev)
2964 [~, iOld] = setdiff({jPlugsPrev.name}, {PlugDesc.Name});
2965 for ix = 1 : length(iOld)
2966 % Find category menu component
2967 jMenuCat = jPlugsPrev(iOld(ix)).menu.getParent.getInvoker;
2968 % Find index in parent
2969 iDel = [];
2970 for ic = 0 : jMenuCat.getMenuComponentCount-1
2971 if jPlugsPrev(iOld(ix)).menu == jMenuCat.getMenuComponent(ic)
2972 iDel = ic;
2973 break
2974 end
2975 end
2976 % Remove from parent
2977 if ~isempty(iDel)
2978 jMenuCat.remove(iDel);
2979 end
2980 end
2981 end
2982 % Create options for adding user-defined plugins
2983 if ~isCompiled && isNewMenu
2984 menuCategory = 'User defined';
2985 jMenuUserDef = [];
2986 for iMenuItem = 0 : jMenu.getItemCount-1
2987 if ~isempty(regexp(jMenu.getMenuComponent(iMenuItem).class, 'JMenu$', 'once')) && strcmp(char(jMenu.getMenuComponent(iMenuItem).getText), menuCategory)
2988 jMenuUserDef = jMenu.getMenuComponent(iMenuItem);
2989 end
2990 end
2991 if isempty(jMenuUserDef)
2992 jMenuUserDef = gui_component('Menu', jMenu, [], menuCategory, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2993 end
2994 jAddUserDefMan = gui_component('MenuItem', [], [], 'Add manually', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('manual'), fontSize);
2995 jAddUserDefFile = gui_component('MenuItem', [], [], 'Add from file', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('file'), fontSize);
2996 jAddUserDefUrl = gui_component('MenuItem', [], [], 'Add from URL', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('url'), fontSize);
2997 jRmvUserDefMan = gui_component('MenuItem', [], [], 'Remove plugin', IconLoader.ICON_DELETE, [], @(h,ev)RemoveUserDefDesc, fontSize);
2998 % Insert "Add" options at the begining of the 'User defined' menu
2999 jMenuUserDef.insert(jAddUserDefMan, 0);
3000 jMenuUserDef.insert(jAddUserDefFile, 1);
3001 jMenuUserDef.insert(jAddUserDefUrl, 2);
3002 jMenuUserDef.insert(jRmvUserDefMan, 3);
3003 jMenuUserDef.insertSeparator(4);
3004 end
3005 % List
3006 if ~isCompiled && isNewMenu
3007 jMenu.addSeparator();
3008 gui_component('MenuItem', jMenu, [], 'List', IconLoader.ICON_EDIT, [], @(h,ev)List('Installed', 1), fontSize);
3009 end
3010 end
3011
3012
3013 %% ===== MENUS: UPDATE =====
3014 function MenuUpdate(jMenu, fontSize)
3015 import org.brainstorm.icon.*;
3016 global GlobalData
3017 % Get installed and supported plugins
3018 [PlugsInstalled, PlugsSupported]= GetInstalled();
3019 % Get previous menu entries
3020 jPlugs = GlobalData.Program.GUI.pluginMenus;
3021 % Regenerate plugin menu to look for new plugins
3022 jPlugs = MenuCreate(jMenu, jPlugs, PlugsSupported, fontSize);
3023 % Update menu entries
3024 GlobalData.Program.GUI.pluginMenus = jPlugs;
3025 % If compiled: disable most menus
3026 isCompiled = bst_iscompiled();
3027 % Interface scaling
3028 InterfaceScaling = bst_get('InterfaceScaling');
3029 % Update all the plugins
3030 for iPlug = 1:length(jPlugs)
3031 j = jPlugs(iPlug);
3032 PlugName = j.name;
3033 Plug = PlugsInstalled(ismember({PlugsInstalled.Name}, PlugName));
3034 PlugRef = PlugsSupported(ismember({PlugsSupported.Name}, PlugName));
3035 % Is installed?
3036 if ~isempty(Plug)
3037 isInstalled = 1;
3038 elseif ~isempty(PlugRef)
3039 Plug = PlugRef;
3040 isInstalled = 0;
3041 else
3042 disp(['BST> Error: Description not found for plugin: ' PlugName]);
3043 continue;
3044 end
3045 isLoaded = isInstalled && Plug.isLoaded;
3046 isManaged = isInstalled && Plug.isManaged;
3047 % Compiled included: no submenus
3048 if isCompiled && (PlugRef.CompiledStatus == 2)
3049 j.menu.setEnabled(1);
3050 if (InterfaceScaling ~= 100)
3051 j.menu.setIcon(IconLoader.scaleIcon(IconLoader.ICON_GOOD, InterfaceScaling / 100));
3052 else
3053 j.menu.setIcon(IconLoader.ICON_GOOD);
3054 end
3055 % Otherwise: all available
3056 else
3057 % Main menu: Available/Not available
3058 j.menu.setEnabled(isInstalled || ~isempty(Plug.URLzip));
3059 % Current version
3060 if ~isInstalled
3061 j.version.setText('<HTML><FONT color="#707070"><I>Not installed</I></FONT>');
3062 elseif ~isManaged && ~isempty(Plug.Path)
3063 j.version.setText('<HTML><FONT color="#707070"><I>Custom install</I></FONT>')
3064 elseif ~isempty(Plug.Version) && ischar(Plug.Version)
3065 strVer = Plug.Version;
3066 % If downloading from github
3067 if isGithubSnapshot(Plug.URLzip)
3068 % Show installation date, if available
3069 if ~isempty(Plug.InstallDate)
3070 strVer = Plug.InstallDate(1:11);
3071 % Show only the short SHA (7 chars)
3072 elseif (length(Plug.Version) >= 30)
3073 strVer = Plug.Version(1:7);
3074 end
3075 end
3076 j.version.setText(['<HTML><FONT color="#707070"><I>Installed version: ' strVer '</I></FONT>'])
3077 elseif isInstalled
3078 j.version.setText('<HTML><FONT color="#707070"><I>Installed</I></FONT>');
3079 end
3080 % Main menu: Icon
3081 if isCompiled && isInstalled
3082 menuIcon = IconLoader.ICON_GOOD;
3083 elseif isLoaded % Loaded
3084 menuIcon = IconLoader.ICON_GOOD;
3085 elseif isInstalled % Not loaded
3086 menuIcon = IconLoader.ICON_BAD;
3087 else
3088 menuIcon = IconLoader.ICON_NEUTRAL;
3089 end
3090 if (InterfaceScaling ~= 100)
3091 j.menu.setIcon(IconLoader.scaleIcon(menuIcon, InterfaceScaling / 100));
3092 else
3093 j.menu.setIcon(menuIcon);
3094 end
3095 % Install
3096 j.install.setEnabled(~isInstalled);
3097 if ~isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
3098 j.install.setText(['<HTML>Install <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
3099 else
3100 j.install.setText('Install');
3101 end
3102 % Update
3103 j.update.setEnabled(isManaged);
3104 if isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
3105 j.update.setText(['<HTML>Update <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
3106 else
3107 j.update.setText('Update');
3108 end
3109 % Uninstall
3110 j.uninstall.setEnabled(isManaged);
3111 % Custom install
3112 j.custom.setEnabled(~isManaged);
3113 if ~isempty(Plug.Path)
3114 j.custompath.setText(Plug.Path);
3115 else
3116 j.custompath.setText('Path not set');
3117 end
3118 % Load/Unload
3119 j.load.setEnabled(isInstalled && ~isLoaded && ~isCompiled);
3120 j.unload.setEnabled(isLoaded && ~isCompiled);
3121 % Web
3122 j.web.setEnabled(~isempty(Plug.URLinfo));
3123 % Extra menus: Update availability
3124 if ~isempty(Plug.ExtraMenus)
3125 for iMenu = 1:size(Plug.ExtraMenus,1)
3126 if (size(Plug.ExtraMenus,2) == 3) && ~isempty(Plug.ExtraMenus{3})
3127 if (strcmpi(Plug.ExtraMenus{3}, 'loaded') && isLoaded) ...
3128 || (strcmpi(Plug.ExtraMenus{3}, 'installed') && isInstalled) ...
3129 || (strcmpi(Plug.ExtraMenus{3}, 'always'))
3130 j.extra(iMenu).setEnabled(1);
3131 else
3132 j.extra(iMenu).setEnabled(0);
3133 end
3134 end
3135 end
3136 end
3137 end
3138 end
3139 j.menu.repaint()
3140 j.menu.getParent().repaint()
3141 end
3142
3143
3144 %% ===== SET CUSTOM PATH =====
3145 function SetCustomPath(PlugName, PlugPath)
3146 % Parse inputs
3147 if (nargin < 2) || isempty(PlugPath)
3148 PlugPath = [];
3149 end
3150 % Custom plugin paths
3151 PluginCustomPath = bst_get('PluginCustomPath');
3152 % Get plugin description
3153 PlugDesc = GetSupported(PlugName);
3154 if isempty(PlugDesc)
3155 return;
3156 end
3157 % Get installed plugin
3158 PlugInst = GetInstalled(PlugName);
3159 isInstalled = ~isempty(PlugInst);
3160 isManaged = isInstalled && PlugInst.isManaged;
3161 if isManaged
3162 bst_error(['Plugin ' PlugName ' is already installed by Brainstorm, uninstall it first.'], 0);
3163 return;
3164 end
3165 % Ask install path to user
3166 isWarning = 1;
3167 if isempty(PlugPath)
3168 PlugPath = uigetdir(PlugInst.Path, ['Select ' PlugName ' directory.']);
3169 if isequal(PlugPath, 0)
3170 PlugPath = [];
3171 end
3172 % If removal is requested
3173 elseif isequal(PlugPath, 0)
3174 PlugPath = [];
3175 isWarning = 0;
3176 end
3177 % If the directory did not change: nothing to do
3178 if (isInstalled && isequal(PlugInst.Path, PlugPath)) || (~isInstalled && isempty(PlugPath))
3179 return;
3180 end
3181 % Unload previous version
3182 if isInstalled && ~isempty(PlugInst.Path) && PlugInst.isLoaded
3183 Unload(PlugName);
3184 end
3185 % Check if this is a valid plugin folder
3186 if isempty(PlugPath) || ~file_exist(PlugPath)
3187 PlugPath = [];
3188 end
3189 if ~isempty(PlugPath) && ~isempty(PlugDesc.TestFile)
3190 isValid = 0;
3191 if file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
3192 isValid = 1;
3193 elseif ~isempty(PlugDesc.LoadFolders)
3194 for iFolder = 1:length(PlugDesc.LoadFolders)
3195 if file_exist(bst_fullfile(PlugPath, PlugDesc.LoadFolders{iFolder}, PlugDesc.TestFile))
3196 isValid = 1;
3197 end
3198 end
3199 end
3200 if ~isValid
3201 PlugPath = [];
3202 end
3203 end
3204 % Save path
3205 PluginCustomPath.(PlugName) = PlugPath;
3206 bst_set('PluginCustomPath', PluginCustomPath);
3207 % Load plugin
3208 if ~isempty(PlugPath)
3209 [isOk, errMsg, PlugDesc] = Load(PlugName);
3210 % Ignored warnings
3211 elseif ~isWarning
3212 isOk = 1;
3213 errMsg = [];
3214 % Invalid path
3215 else
3216 isOk = 0;
3217 if ~isempty(PlugDesc.TestFile)
3218 errMsg = ['The file ' PlugDesc.TestFile ' could not be found in selected folder.'];
3219 else
3220 errMsg = 'No valid folder was found.';
3221 end
3222 end
3223 % Handle errors
3224 if ~isOk
3225 bst_error(['An error occurred while configuring plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
3226 elseif ~isempty(errMsg)
3227 java_dialog('msgbox', ['Configuration message:' 10 10 errMsg 10], 'Plugin manager');
3228 elseif isWarning
3229 java_dialog('msgbox', ['Plugin ' PlugName ' successfully loaded.']);
3230 end
3231 end
3232
3233
3234 %% ===== ARCHIVE SOFTWARE ENVIRONMENT =====
3235 % USAGE: Archive(OutputFile=[ask])
3236 function Archive(OutputFile)
3237 % Parse inputs
3238 if (nargin < 1) || isempty(OutputFile)
3239 OutputFile = [];
3240 end
3241 % Get date string
3242 c = clock();
3243 strDate = sprintf('%02d%02d%02d', c(1)-2000, c(2), c(3));
3244 % Get output filename
3245 if isempty(OutputFile)
3246 % Get default directories
3247 LastUsedDirs = bst_get('LastUsedDirs');
3248 % Default output filename
3249 OutputFile = bst_fullfile(LastUsedDirs.ExportScript, ['bst_env_' strDate '.zip']);
3250 % File selection
3251 OutputFile = java_getfile('save', 'Export environment', OutputFile, 'single', 'files', ...
3252 {{'.zip'}, 'Zip files (*.zip)', 'ZIP'}, 1);
3253 if isempty(OutputFile)
3254 return
3255 end
3256 % Save new default export path
3257 LastUsedDirs.ExportScript = bst_fileparts(OutputFile);
3258 bst_set('LastUsedDirs', LastUsedDirs);
3259 end
3260
3261 % ===== TEMP FOLDER =====
3262 bst_progress('start', 'Export environment', 'Creating temporary folder...');
3263
3264 % ===== COPY BRAINSTORM =====
3265 bst_progress('text', 'Copying: brainstorm...');
3266 % Get Brainstorm path and version
3267 bstVer = bst_get('Version');
3268 bstDir = bst_get('BrainstormHomeDir');
3269 % Create temporary folder for storing all the files to package
3270 TmpDir = bst_get('BrainstormTmpDir', 0, 'bstenv');
3271 % Get brainstorm3 destination folder: add version number
3272 if ~isempty(bstVer.Version) && ~any(bstVer.Version == '?')
3273 envBst = bst_fullfile(TmpDir, ['brainstorm', bstVer.Version]);
3274 else
3275 [tmp, bstName] = bst_fileparts(bstDir);
3276 envBst = bst_fullfile(TmpDir, bstName);
3277 end
3278 % Add git commit hash
3279 if (length(bstVer.Commit) >= 30)
3280 envBst = [envBst, '_', bstVer.Commit(1:7)];
3281 end
3282 % Copy brainstorm3 folder
3283 isOk = file_copy(bstDir, envBst);
3284 if ~isOk
3285 error(['Cannot copy folder: "' bstDir '" to "' envBst '"']);
3286 end
3287
3288 % ===== COPY DEFAULTS =====
3289 bst_progress('text', 'Copying: user defaults...');
3290 % Get user defaults folder
3291 userDef = bst_get('UserDefaultsDir');
3292 envDef = bst_fullfile(envBst, 'defaults');
3293 isOk = file_copy(userDef, envDef);
3294 if ~isOk
3295 error(['Cannot merge folder: "' userDef '" into "' envDef '"']);
3296 end
3297
3298 % ===== COPY USER PROCESSES =====
3299 bst_progress('text', 'Copying: user processes...');
3300 % Get user process folder
3301 userProc = bst_get('UserProcessDir');
3302 envProc = bst_fullfile(envBst, 'toolbox', 'process', 'functions');
3303 isOk = file_copy(userProc, envProc);
3304 if ~isOk
3305 error(['Cannot merge folder: "' userProc '" into "' envProc '"']);
3306 end
3307
3308 % ===== COPY PLUGINS ======
3309 % Get list of plugins to package
3310 PlugDesc = GetInstalled();
3311 % Destination plugin directory
3312 envPlugins = bst_fullfile(envBst, 'plugins');
3313 % Copy each installed plugin
3314 for iPlug = 1:length(PlugDesc)
3315 bst_progress('text', ['Copying plugin: ' PlugDesc(iPlug).Name '...']);
3316 envPlug = bst_fullfile(envPlugins, PlugDesc(iPlug).Name);
3317 isOk = file_copy(PlugDesc(iPlug).Path, envPlug);
3318 if ~isOk
3319 error(['Cannot copy folder: "' PlugDesc(iPlug).Path '" into "' envProc '"']);
3320 end
3321 end
3322 % Copy user-defined JSON files
3323 PlugJson = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
3324 for iPlugJson = 1:length(PlugJson)
3325 bst_progress('text', ['Copying use-defined plugin JSON file: ' PlugJson(iPlugJson).name '...']);
3326 plugJsonFile = bst_fullfile(PlugJson(iPlugJson).folder, PlugJson(iPlugJson).name);
3327 envPlugJson = bst_fullfile(envPlugins, PlugJson(iPlugJson).name);
3328 isOk = file_copy(plugJsonFile, envPlugJson);
3329 if ~isOk
3330 error(['Cannot copy file: "' plugJsonFile '" into "' envProc '"']);
3331 end
3332 end
3333
3334 % ===== SAVE LIST OF VERSIONS =====
3335 strList = bst_plugin('List', 'installed', 0);
3336 % Open file versions.txt
3337 VersionFile = bst_fullfile(TmpDir, 'versions.txt');
3338 fid = fopen(VersionFile, 'wt');
3339 if (fid < 0)
3340 error(['Cannot save file: ' VersionFile]);
3341 end
3342 % Save Brainstorm plugins list
3343 fwrite(fid, strList);
3344 % Save Matlab ver command
3345 strMatlab = evalc('ver');
3346 fwrite(fid, [10 10 strMatlab]);
3347 % Close file
3348 fclose(fid);
3349
3350 % ===== ZIP FILES =====
3351 bst_progress('text', 'Zipping environment...');
3352 % Zip files with bst_env_* being the first level
3353 zip(OutputFile, TmpDir, bst_fileparts(TmpDir));
3354 % Delete the temporary files
3355 file_delete(TmpDir, 1, 1);
3356 % Close progress bar
3357 bst_progress('stop');
3358 end
3359
3360
3361 %% ============================================================================
3362 % ===== PLUGIN-SPECIFIC FUNCTIONS ============================================
3363 % ============================================================================
3364
3365 %% ===== LINK TOOLBOX-SPM =====
3366 % USAGE: bst_plugin('LinkSpmToolbox', Action)
3367 % 0=Delete/1=Create/2=Check a symbolic link for a Toolbox in SPM12 toolbox folder
3368 function LinkSpmToolbox(Action, ToolboxName)
3369 % Get SPM12 plugin
3370 PlugSpm = GetInstalled('spm12');
3371 if isempty(PlugSpm)
3372 error('Plugin SPM12 is not loaded.');
3373 elseif ~PlugSpm.isLoaded
3374 [isOk, errMsg, PlugSpm] = Load('spm12');
3375 if ~isOk
3376 error('Plugin SPM12 cannot be loaded.');
3377 end
3378 end
3379 % Get SPM plugin path
3380 if ~isempty(PlugSpm.SubFolder)
3381 spmToolboxDir = bst_fullfile(PlugSpm.Path, PlugSpm.SubFolder, 'toolbox');
3382 else
3383 spmToolboxDir = bst_fullfile(PlugSpm.Path, 'toolbox');
3384 end
3385 if ~file_exist(spmToolboxDir)
3386 error(['Could not find SPM12 toolbox folder: ' spmToolboxDir]);
3387 end
3388 % Toolbox plugin path
3389 spmToolboxDirTarget = bst_fullfile(spmToolboxDir, ToolboxName);
3390 % Get toolbox plugin
3391 PlugToolbox = GetInstalled(ToolboxName);
3392
3393 % Check link
3394 if (Action == 2)
3395 % Link exists and works: return here
3396 if file_exist(bst_fullfile(spmToolboxDirTarget, PlugToolbox.TestFile))
3397 return;
3398 % Link doesn't exist: Create it
3399 else
3400 Action = 1;
3401 end
3402 end
3403 % If folder already exists
3404 if file_exist(spmToolboxDirTarget)
3405 % If setting install and SPM is not managed by Brainstorm: do not risk deleting user's install
3406 if (Action == 1) && ~PlugSpm.isManaged
3407 error([upper(ToolboxName) ' seems already set up: ' spmToolboxDirTarget]);
3408 end
3409 % All the other cases: delete existing toolbox folder
3410 if ispc
3411 rmCall = ['rmdir /q /s "' spmToolboxDirTarget '"'];
3412 else
3413 rmCall = ['rm -rf "' spmToolboxDirTarget '"'];
3414 end
3415 disp(['BST> Deleting existing SPM12 toolbox: ' rmCall]);
3416 [status,result] = system(rmCall);
3417 if (status ~= 0)
3418 error(['Error deleting link: ' result]);
3419 end
3420 end
3421 % Create new link
3422 if (Action == 1)
3423 if isempty(PlugToolbox) || ~PlugToolbox.isLoaded
3424 error(['Plugin ' upper(ToolboxName) ' is not loaded.']);
3425 end
3426 % Return if installation is not complete yet (first load before installation ends)
3427 if isempty(PlugToolbox.InstallDate)
3428 return
3429 end
3430 % Define source and target for the link
3431 if ~isempty(PlugToolbox.SubFolder)
3432 linkTarget = bst_fullfile(PlugToolbox.Path, PlugToolbox.SubFolder);
3433 else
3434 linkTarget = PlugToolbox.Path;
3435 end
3436 linkFile = spmToolboxDirTarget;
3437 % Create link
3438 if ispc
3439 linkCall = ['mklink /D "' linkFile '" "' linkTarget '"'];
3440 else
3441 linkCall = ['ln -s "' linkTarget '" "' linkFile '"'];
3442 end
3443 disp(['BST> Creating symbolic link: ' linkCall]);
3444 [status,result] = system(linkCall);
3445 if (status ~= 0)
3446 error(['Error creating link: ' result]);
3447 end
3448 end
3449 end
3450
3451
3452 %% ===== SET PROGRESS LOGO =====
3453 % USAGE: SetProgressLogo(PlugDesc/PlugName) % Set progress bar image
3454 % SetProgressLogo([]) % Remove progress bar image
3455 function SetProgressLogo(PlugDesc)
3456 % Remove image
3457 if (nargin < 1) || isempty(PlugDesc)
3458 bst_progress('removeimage');
3459 % Set image
3460 else
3461 bst_progress('setpluginlogo', PlugDesc);
3462 end
3463 end
3464
3465
3466 %% ===== NOT SUPPORTED APPLE SILICON =====
3467 % Return list of plugins not supported on Apple silicon
3468 function pluginNames = PluginsNotSupportAppleSilicon()
3469 pluginNames = { 'duneuro', 'mcxlab-cuda'};
3470 end
3471
3472 %% ===== MATCH STRING EDGES =====
3473 % Check if a string 'strA' starts (or ends) with string B
3474 function result = strMatchEdge(a, b, edge)
3475 b = regexptranslate('escape', b);
3476 if strcmpi(edge, 'start')
3477 result = ~isempty(regexp(a, ['^', b], 'once'));
3478 elseif strcmpi(edge, 'end')
3479 result = ~isempty(regexp(a, [b, '$'], 'once'));
3480 else
3481 result = 0;
3482 end
3483 end