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