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