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