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