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