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.
Custom install: If FieldTrip is somewhere on your computer, but not currently in your Matlab path, you can simply tell Brainstorm where it is with the menu: Plugins > fieldtrip > Custom install > Set installation folder.
Plugin install: Otherwise, the easiest solution is to click on Plugins > fieldtrip > Install, and let Brainstorm manage everything automatically: downloading the latest version, setting the path, managing incompatibilities with other toolboxes (e.g. SPM or ROAST).
Plugin definition
The plugins registered in Brainstorm are listed in bst_plugin.m / GetSupported. Each one is an entry in the PlugDesc array, following the structure defined in db_template('plugdesc'). The fields allowed are described below.
Mandatory fields:
Name: String: Plugin name = subfolder in the Brainstorm user folder
Version: String: Version of the plugin (eg. '1.2', '21a', 'github-master', 'latest')
URLzip: String: Download URL, zip or tgz file accessible over HTTP/HTTPS/FTP
URLinfo: String: Information URL = Software website
Optional fields:
AutoUpdate: Boolean: If true, the plugin is updated automatically when there is a new version available (default: 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 'if exist(''filtfilt'', ''file''), ft_default.toolbox.signal=''matlab''; end; ' ...
750 'if exist(''nansum'', ''file''), ft_default.toolbox.stats=''matlab''; end; ' ...
751 'if exist(''rgb2hsv'', ''file''), ft_default.toolbox.images=''matlab''; end; ' ...
752 'ft_defaults;'];
753
754 % === SPM12 ===
755 PlugDesc(end+1) = GetStruct('spm12');
756 PlugDesc(end).Version = 'latest';
757 PlugDesc(end).AutoUpdate = 0;
758 switch(OsType)
759 case 'mac64arm'
760 PlugDesc(end).URLzip = 'https://github.com/spm/spm12/archive/refs/heads/maint.zip';
761 PlugDesc(end).Version = 'github-maint';
762 otherwise
763 PlugDesc(end).Version = 'latest';
764 PlugDesc(end).URLzip = 'https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip';
765 end
766 PlugDesc(end).URLinfo = 'https://www.fil.ion.ucl.ac.uk/spm/';
767 PlugDesc(end).TestFile = 'spm.m';
768 PlugDesc(end).ReadmeFile = 'README.md';
769 PlugDesc(end).CompiledStatus = 2;
770 PlugDesc(end).UnloadPlugs = {'fieldtrip', 'roast'};
771 PlugDesc(end).LoadFolders = {'matlabbatch'};
772 PlugDesc(end).GetVersionFcn = 'bst_getoutvar(2, @spm, ''Ver'')';
773 PlugDesc(end).LoadedFcn = 'spm(''defaults'',''EEG'');';
774
775 % === USER DEFINED PLUGINS ===
776 plugJsonFiles = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
777 badJsonFiles = {};
778 plugUserDefNames = {};
779 for ix = 1:length(plugJsonFiles)
780 plugJsonText = fileread(fullfile(plugJsonFiles(ix).folder, plugJsonFiles(ix).name));
781 try
782 PlugUserDesc = bst_jsondecode(plugJsonText);
783 catch
784 badJsonFiles{end+1} = plugJsonFiles(ix).name;
785 continue
786 end
787 % Reshape fields "ExtraMenus"
788 if isfield(PlugUserDesc, 'ExtraMenus') && ~isempty(PlugUserDesc.ExtraMenus) && iscell(PlugUserDesc.ExtraMenus{1})
789 PlugUserDesc.ExtraMenus = cat(2, PlugUserDesc.ExtraMenus{:})';
790 end
791 % Reshape fields "RequiredPlugs"
792 if isfield(PlugUserDesc, 'RequiredPlugs') && ~isempty(PlugUserDesc.RequiredPlugs) && iscell(PlugUserDesc.RequiredPlugs{1})
793 PlugUserDesc.RequiredPlugs = cat(2, PlugUserDesc.RequiredPlugs{:})';
794 end
795 % Check for uniqueness for user-defined plugin
796 if ~ismember(PlugUserDesc.Name, {PlugDesc.Name})
797 plugUserDefNames{end+1} = PlugUserDesc.Name;
798 PlugDesc(end+1) = struct_copy_fields(GetStruct(PlugUserDesc.Name), PlugUserDesc);
799 end
800 end
801 % Print info on user-defined plugins
802 if UserDefVerbose
803 if ~isempty(plugUserDefNames)
804 fprintf(['BST> User-defined plugins... ' strjoin(plugUserDefNames, ' ') '\n']);
805 end
806 for iBad = 1 : length(badJsonFiles)
807 fprintf(['BST> User-defined plugins, error reading .json file... ' badJsonFiles{iBad} '\n']);
808 end
809 end
810
811 % ================================================================================================================
812
813 % Select only one plugin
814 if ~isempty(SelPlug)
815 % Get plugin name
816 if ischar(SelPlug)
817 PlugName = SelPlug;
818 else
819 PlugName = SelPlug.Name;
820 end
821 % Find in the list of plugins
822 iPlug = find(strcmpi({PlugDesc.Name}, PlugName));
823 if ~isempty(iPlug)
824 PlugDesc = PlugDesc(iPlug);
825 else
826 PlugDesc = [];
827 end
828 end
829 end
830
831
832 %% ===== PLUGIN STRUCT =====
833 function s = GetStruct(PlugName)
834 s = db_template('PlugDesc');
835 s.Name = PlugName;
836 end
837
838
839 %% ===== ADD USER DEFINED PLUGIN DESCRIPTION =====
840 function [isOk, errMsg] = AddUserDefDesc(RegMethod, jsonLocation)
841 isOk = 1;
842 errMsg = '';
843 isInteractive = strcmp(RegMethod, 'manual') || nargin < 2 || isempty(jsonLocation);
844
845 % Get json file location from user
846 if ismember(RegMethod, {'file', 'url'}) && isInteractive
847 if strcmp(RegMethod, 'file')
848 jsonLocation = java_getfile('open', 'Plugin description JSON file...', '', 'single', 'files', {{'.json'}, 'Brainstorm plugin description (*.json)', 'JSON'}, 1);
849 elseif strcmp(RegMethod, 'url')
850 jsonLocation = java_dialog('input', 'Enter the URL the plugin description file (.json)', 'Plugin description JSON file...', [], '');
851 end
852 if isempty(jsonLocation)
853 return
854 end
855 res = java_dialog('question', ['Warning: This plugin has not been verified.' 10 ...
856 'Malicious plugins can alter your database, proceed with caution and only install plugins from trusted sources.' 10 ...
857 'If any unusual behavior occurs after installation, start by uninstalling the plugins.' 10 ...
858 'Are you sure you want to proceed?'], ...
859 'Warning', [], {'yes', 'no'});
860 if strcmp(res, 'no')
861 return
862 end
863 end
864
865 % Get plugin description
866 switch RegMethod
867 case 'file'
868 jsonText = fileread(jsonLocation);
869 try
870 PlugDesc = bst_jsondecode(jsonText);
871 catch
872 errMsg = sprintf(['Could not parse JSON file:' 10 '%s'], jsonLocation);
873 end
874
875 case 'url'
876 % Handle GitHub links, convert the link to load the raw content
877 if strcmp(jsonLocation(1:4),'http') && strcmp(jsonLocation(end-4:end),'.json')
878 if ~isempty(regexp(jsonLocation, '^http[s]*://github.com', 'once'))
879 jsonLocation = strrep(jsonLocation, 'github.com','raw.githubusercontent.com');
880 jsonLocation = strrep(jsonLocation, 'blob/', '');
881 end
882 end
883 jsonText = bst_webread(jsonLocation);
884 try
885 PlugDesc = bst_jsondecode(jsonText);
886 catch
887 errMsg = sprintf(['Could not parse JSON file at:' 10 '%s'], jsonLocation);
888 end
889
890 case 'manual'
891 % Get info for user-defined plugin description from user
892 res = java_dialog('input', { ['<HTML>Provide the <B>mandatory</B> fields for a user defined Brainstorm plugin<BR>' ...
893 'See this page for further details:<BR>' ...
894 '<FONT COLOR="#0000FF">https://neuroimage.usc.edu/brainstorm/Tutorials/Plugins</FONT>' ...
895 '<BR><BR>' ...
896 'Plugin name<BR>' ...
897 '<I><FONT color="#707070">EXAMPLE: bst-users</FONT></I>'], ...
898 ['<HTML>Version<BR>' ...
899 '<I><FONT color="#707070">EXAMPLE: github-main or 3.1.4</FONT></I>'], ...
900 ['<HTML>URL for zip<BR>' ...
901 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users/archive/refs/heads/master.zip</FONT></I>'], ...
902 ['<HTML>URL for information<BR>' ...
903 '<I><FONT color="#707070">EXAMPLE: https://github.com/brainstorm-tools/bst-users</FONT></I>']}, ...
904 'User defined plugin', [], {'', '', '', ''});
905 if isempty(res) || any(cellfun(@isempty,res))
906 return
907 end
908 PlugDesc.Name = lower(res{1});
909 PlugDesc.Version = res{2};
910 PlugDesc.URLzip = res{3};
911 PlugDesc.URLinfo = res{4};
912 end
913 if ~isempty(errMsg)
914 bst_error(errMsg);
915 isOk = 0;
916 return;
917 end
918
919 % Validate retrieved plugin description
920 if length(PlugDesc) > 1
921 errMsg = 'JSON file should contain only one plugin description';
922 elseif ~all(ismember({'Name', 'Version', 'URLzip', 'URLinfo'}, fieldnames(PlugDesc)))
923 errMsg = 'Plugin description must contain the fields ''Name'', ''Version'', ''URLzip'' and ''URLinfo''';
924 else
925 PlugDesc.Name = lower(PlugDesc.Name);
926 PlugDescs = GetSupported();
927 if ismember(PlugDesc.Name, {PlugDescs.Name})
928 errMsg = sprintf('Plugin ''%s'' already exist in Brainstorm', PlugDesc.Name);
929 end
930 end
931 if ~isempty(errMsg)
932 bst_error(errMsg);
933 isOk = 0;
934 return;
935 end
936 % Override category
937 PlugDesc.Category = 'User defined';
938
939 % Write validated JSON file
940 pluginJsonFileOut = fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name)));
941 fid = fopen(pluginJsonFileOut, 'wt');
942 jsonText = bst_jsonencode(PlugDesc, 0);
943 fprintf(fid, jsonText);
944 fclose(fid);
945
946 fprintf(1, 'BST> Plugin ''%s'' was added to ''User defined'' plugins\n', PlugDesc.Name);
947 end
948
949
950 %% ===== REMOVE USER DEFINED PLUGIN DESCRIPTION =====
951 function [isOk, errMsg] = RemoveUserDefDesc(PlugName)
952 isOk = 1;
953 errMsg = '';
954 if nargin < 1 || isempty(PlugName)
955 PlugDescs = GetSupported();
956 PlugDescs = PlugDescs(ismember({PlugDescs.Category}, 'User defined'));
957 PlugName = java_dialog('combo', 'Indicate the name of the plugin to remove:', 'Remove plugin from ''User defined'' list', [], {PlugDescs.Name});
958 end
959 if isempty(PlugName)
960 return
961 end
962 PlugDesc = GetSupported(PlugName);
963 if ~isempty(PlugDesc.Path) || file_exist(bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name))
964 [isOk, errMsg] = Uninstall(PlugDesc.Name, 0);
965 end
966 % Delete json file
967 if isOk
968 isOk = file_delete(fullfile(bst_get('UserPluginsDir'), sprintf('plugin_%s.json', file_standardize(PlugDesc.Name))), 1);
969 end
970
971 fprintf(1, 'BST> Plugin ''%s'' was removed from ''User defined'' plugins\n', PlugDesc.Name);
972 end
973
974
975 %% ===== CONFIGURE PLUGIN =====
976 function Configure(PlugDesc)
977 switch (PlugDesc.Name)
978 case 'mff'
979 % Add .jar file to static classpath
980 if ~exist('com.egi.services.mff.api.MFFFactory', 'class')
981 jarList = dir(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'MFF-*.jar'));
982 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, jarList(1).name);
983 disp(['BST> Adding to Java classpath: ' jarPath]);
984 warning off
985 javaaddpathstatic(jarPath);
986 javaaddpath(jarPath);
987 warning on
988 end
989
990 case 'nwb'
991 % Add .jar file to static classpath
992 if ~exist('Schema', 'class')
993 jarPath = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'jar', 'schema.jar');
994 disp(['BST> Adding to Java classpath: ' jarPath]);
995 warning off
996 javaaddpathstatic(jarPath);
997 javaaddpath(jarPath);
998 warning on
999 schema = Schema();
1000 end
1001 % Go to NWB folder
1002 curDir = pwd;
1003 cd(bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder));
1004 % Generate the NWB Schema (must be executed from the NWB folder)
1005 generateCore();
1006 % Restore current directory
1007 cd(curDir);
1008 end
1009 end
1010
1011
1012 %% ===== GET ONLINE VERSION =====
1013 % Get the latest online version of some plugins
1014 function [Version, URLzip] = GetVersionOnline(PlugName, URLzip, isCache)
1015 global GlobalData;
1016 Version = [];
1017 % Parse inputs
1018 if (nargin < 2) || isempty(URLzip)
1019 URLzip = [];
1020 end
1021 % Use cache by default, to avoid fetching online too many times the same info
1022 if (nargin < 3) || isempty(isCache)
1023 isCache = 1;
1024 end
1025 % No internet: skip
1026 if ~GlobalData.Program.isInternet
1027 return;
1028 end
1029 % Check for existing plugin cache
1030 strCache = [PlugName, '_online_', strrep(date,'-','')];
1031 if isCache && isfield(GlobalData.Program.PluginCache, strCache) && isfield(GlobalData.Program.PluginCache.(strCache), 'Version')
1032 Version = GlobalData.Program.PluginCache.(strCache).Version;
1033 URLzip = GlobalData.Program.PluginCache.(strCache).URLzip;
1034 return;
1035 end
1036 % Get version online
1037 try
1038 switch (PlugName)
1039 case 'spm12'
1040 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1041 disp(['BST> Checking latest online version for ' PlugName '...']);
1042 s = bst_webread('https://www.fil.ion.ucl.ac.uk/spm/download/spm12_updates/');
1043 if ~isempty(s)
1044 n = regexp(s,'spm12_updates_r(\d.*?)\.zip','tokens','once');
1045 if ~isempty(n) && ~isempty(n{1})
1046 Version = n{1};
1047 end
1048 end
1049 case 'cat12'
1050 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1051 disp(['BST> Checking latest online version for ' PlugName '...']);
1052 s = bst_webread('https://www.neuro.uni-jena.de/cat12/');
1053 if ~isempty(s)
1054 n = regexp(s,'cat12_r(\d.*?)\.zip','tokens');
1055 if ~isempty(n)
1056 Version = max(cellfun(@str2double, [n{:}]));
1057 Version = num2str(Version);
1058 end
1059 end
1060 case 'fieldtrip'
1061 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1062 disp(['BST> Checking latest online version for ' PlugName '...']);
1063 s = bst_webread('https://download.fieldtriptoolbox.org');
1064 if ~isempty(s)
1065 n = regexp(s,'fieldtrip-lite-(\d.*?)\.zip','tokens');
1066 if ~isempty(n)
1067 Version = max(cellfun(@str2double, [n{:}]));
1068 Version = num2str(Version);
1069 URLzip = ['https://download.fieldtriptoolbox.org/fieldtrip-lite-' Version '.zip'];
1070 end
1071 end
1072 case 'duneuro'
1073 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1074 disp(['BST> Checking latest online version for ' PlugName '...']);
1075 str = bst_webread('https://neuroimage.usc.edu/bst/getversion_duneuro.php');
1076 Version = str(1:6);
1077 case 'nirstorm'
1078 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1079 disp(['BST> Checking latest online version for ' PlugName '...']);
1080 str = bst_webread('https://raw.githubusercontent.com/Nirstorm/nirstorm/master/bst_plugin/VERSION');
1081 Version = strtrim(str(9:end));
1082 case 'brainentropy'
1083 bst_progress('text', ['Checking latest online version for ' PlugName '...']);
1084 disp(['BST> Checking latest online version for ' PlugName '...']);
1085 str = bst_webread('https://raw.githubusercontent.com/multifunkim/best-brainstorm/master/best/VERSION.txt');
1086 str = strsplit(str,'\n');
1087 Version = strtrim(str{1});
1088 otherwise
1089 % If downloading from github: Get last GitHub commit SHA
1090 if isGithubMaster(URLzip)
1091 Version = GetGithubCommit(URLzip);
1092 else
1093 return;
1094 end
1095 end
1096 % Executed only if the version was fetched successfully: Keep cached version
1097 GlobalData.Program.PluginCache.(strCache).Version = Version;
1098 GlobalData.Program.PluginCache.(strCache).URLzip = URLzip;
1099 catch
1100 disp(['BST> Error: Could not get online version for plugin: ' PlugName]);
1101 end
1102 end
1103
1104
1105 %% ===== IS GITHUB MASTER ======
1106 % Returns 1 if the URL is a github master/main branch
1107 function isMaster = isGithubMaster(URLzip)
1108 isMaster = strMatchEdge(URLzip, 'https://github.com/', 'start') && ...
1109 (strMatchEdge(URLzip, 'master.zip', 'end') || strMatchEdge(URLzip, 'main.zip', 'end'));
1110 end
1111
1112
1113 %% ===== GET GITHUB COMMIT =====
1114 % Get SHA of the GitHub HEAD commit
1115 function sha = GetGithubCommit(URLzip)
1116 zipUri = matlab.net.URI(URLzip);
1117 % Primary branch name: master or main
1118 [~, primaryBranch] = bst_fileparts(char(zipUri.Path(end)));
1119 % Default result
1120 sha = ['github-', primaryBranch];
1121 % Only available after Matlab 2016b (because of matlab.net.http.RequestMessage)
1122 if (bst_get('MatlabVersion') < 901)
1123 return;
1124 end
1125 % Try getting the SHA from the GitHub API
1126 try
1127 % Get GitHub repository path
1128 zipUri = matlab.net.URI(URLzip);
1129 gitUser = char(zipUri.Path(2));
1130 gitRepo = char(zipUri.Path(3));
1131 % Request last commit SHA with GitHub API
1132 apiUri = matlab.net.URI(['https://api.github.com/repos/' gitUser '/' gitRepo '/commits/' primaryBranch]);
1133 request = matlab.net.http.RequestMessage;
1134 request = request.addFields(matlab.net.http.HeaderField('Accept', 'application/vnd.github.VERSION.sha'));
1135 r = send(request, apiUri);
1136 sha = char(r.Body.Data);
1137 catch
1138 disp(['BST> Warning: Could not get GitHub version for URL: ' zipUrl]);
1139 end
1140 end
1141
1142
1143 %% ===== COMPARE VERSIONS =====
1144 % Returns: 0: v1==v2
1145 % -1: v1<v2
1146 % 1: v1>v2
1147 function res = CompareVersions(v1, v2)
1148 % Get numbers
1149 iNum1 = find(ismember(v1, '0123456789'));
1150 iNum2 = find(ismember(v2, '0123456789'));
1151 iDot1 = find(v1 == '.');
1152 iDot2 = find(v2 == '.');
1153 % Equality (or one input empty)
1154 if isequal(v1,v2) || isempty(v1) || isempty(v2)
1155 res = 0;
1156 % Only numbers
1157 elseif (length(iNum1) == length(v1)) && (length(iNum2) == length(v2))
1158 n1 = str2double(v1);
1159 n2 = str2double(v2);
1160 if (n1 > n2)
1161 res = 1;
1162 elseif (n1 < n2)
1163 res = -1;
1164 else
1165 res = 0;
1166 end
1167 % Format '1.2.3'
1168 elseif (~isempty(iDot1) || ~isempty(iDot2)) && ~isempty(iNum1) && ~isempty(iNum2)
1169 % Get subversions 1
1170 split1 = str_split(v1, '.');
1171 sub1 = [];
1172 for i = 1:length(split1)
1173 t = str2num(split1{i}(ismember(split1{i},'0123456789')));
1174 if ~isempty(t)
1175 sub1(end+1) = t;
1176 else
1177 break;
1178 end
1179 end
1180 % Get subversions 1
1181 split2 = str_split(v2, '.');
1182 sub2 = [];
1183 for i = 1:length(split2)
1184 t = str2num(split2{i}(ismember(split2{i},'0123456789')));
1185 if ~isempty(t)
1186 sub2(end+1) = t;
1187 else
1188 break;
1189 end
1190 end
1191 % Add extra zeros to the shortest (so that "1.2" is higher than "1")
1192 if (length(sub1) < length(sub2))
1193 tmp = sub1;
1194 sub1 = zeros(size(sub2));
1195 sub1(1:length(tmp)) = tmp;
1196 elseif (length(sub1) > length(sub2))
1197 tmp = sub2;
1198 sub2 = zeros(size(sub1));
1199 sub2(1:length(tmp)) = tmp;
1200 end
1201 % Compare number by number
1202 for i = 1:length(sub1)
1203 if (sub1(i) > sub2(i))
1204 res = 1;
1205 return;
1206 elseif (sub1(i) < sub2(i))
1207 res = -1;
1208 return;
1209 else
1210 res = 0;
1211 end
1212 end
1213 % Mixture of numbers and digits: natural sorting of strings
1214 else
1215 [s,I] = sort_nat({v1, v2});
1216 if (I(1) == 1)
1217 res = -1;
1218 else
1219 res = 1;
1220 end
1221 end
1222 end
1223
1224
1225 %% ===== EXECUTE CALLBACK =====
1226 function [isOk, errMsg] = ExecuteCallback(PlugDesc, f)
1227 isOk = 0;
1228 errMsg = '';
1229 if ~isempty(PlugDesc.(f))
1230 try
1231 if ischar(PlugDesc.(f))
1232 disp(['BST> Executing callback ' f ': ' PlugDesc.(f)]);
1233 eval(PlugDesc.(f));
1234 elseif isa(PlugDesc.(f), 'function_handle')
1235 disp(['BST> Executing callback ' f ': ' func2str(PlugDesc.(f))]);
1236 feval(PlugDesc.(f), PlugDesc);
1237 end
1238 catch
1239 errMsg = ['Error executing callback ' f ': ' 10 lasterr];
1240 return;
1241 end
1242 end
1243 isOk = 1;
1244 end
1245
1246
1247 %% ===== GET INSTALLED PLUGINS =====
1248 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled', PlugName/PlugDesc) % Get one installed plugin
1249 % [PlugDesc, SearchPlugs] = bst_plugin('GetInstalled') % Get all installed plugins
1250 function [PlugDesc, SearchPlugs] = GetInstalled(SelPlug)
1251 % Parse inputs
1252 if (nargin < 1) || isempty(SelPlug)
1253 SelPlug = [];
1254 end
1255
1256 % === DEFINE SEARCH LIST ===
1257 % Looking for a single plugin
1258 if ~isempty(SelPlug)
1259 SearchPlugs = GetSupported(SelPlug);
1260 % Looking for all supported plugins
1261 else
1262 SearchPlugs = GetSupported();
1263 end
1264 % Brainstorm plugin folder
1265 UserPluginsDir = bst_get('UserPluginsDir');
1266 % Custom plugin paths
1267 PluginCustomPath = bst_get('PluginCustomPath');
1268 % Matlab path
1269 matlabPath = str_split(path, pathsep);
1270 % Compiled distribution
1271 isCompiled = bst_iscompiled();
1272
1273 % === LOOK FOR SUPPORTED PLUGINS ===
1274 % Empty plugin structure
1275 PlugDesc = repmat(db_template('PlugDesc'), 0);
1276 % Look for each plugin in the search list
1277 for iSearch = 1:length(SearchPlugs)
1278 % Compiled: skip plugins that are not available
1279 if isCompiled && (SearchPlugs(iSearch).CompiledStatus == 0)
1280 continue;
1281 end
1282 % Theoretical plugin path
1283 PlugName = SearchPlugs(iSearch).Name;
1284 PlugPath = bst_fullfile(UserPluginsDir, PlugName);
1285 % Handle case symbolic link
1286 try
1287 PlugPath = builtin('_canonicalizepath', PlugPath);
1288 catch
1289 % Nothing here
1290 end
1291 % Check if test function is available in the Matlab path
1292 TestFilePath = GetTestFilePath(SearchPlugs(iSearch));
1293 % If installed software found in Matlab path
1294 if ~isempty(TestFilePath)
1295 % Register loaded plugin
1296 iPlug = length(PlugDesc) + 1;
1297 PlugDesc(iPlug) = SearchPlugs(iSearch);
1298 PlugDesc(iPlug).isLoaded = 1;
1299 % Check if the file is inside the Brainstorm user folder (where it is supposed to be) => Managed plugin
1300 if strMatchEdge(TestFilePath, PlugPath, 'start')
1301 PlugDesc(iPlug).isManaged = 1;
1302 % Process compiled together with Brainstorm
1303 elseif isCompiled && ~isempty(strfind(TestFilePath, ['.brainstorm' filesep 'plugins' filesep PlugName]))
1304 compiledDir = ['.brainstorm' filesep 'plugins' filesep PlugName];
1305 iPath = strfind(TestFilePath, compiledDir);
1306 PlugPath = [TestFilePath(1:iPath-2), filesep, compiledDir];
1307 % Otherwise: Custom installation
1308 else
1309 % If the test file was found in a defined subfolder: remove the subfolder from the plugin path
1310 PlugPath = TestFilePath;
1311 for iSub = 1:length(PlugDesc(iPlug).LoadFolders)
1312 subDir = strrep(PlugDesc(iPlug).LoadFolders{iSub}, '/', filesep);
1313 if (length(PlugPath) > length(subDir)) && isequal(PlugPath(end-length(subDir)+1:end), subDir)
1314 PlugPath = PlugPath(1:end - length(subDir) - 1);
1315 break;
1316 end
1317 end
1318 PlugDesc(iPlug).isManaged = 0;
1319 % Look for process_* functions in the process folder
1320 PlugProc = file_find(PlugPath, 'process_*.m', Inf, 0);
1321 if ~isempty(PlugProc)
1322 % Remove absolute path: use only path relative to the plugin Path
1323 PlugDesc(iPlug).Processes = cellfun(@(c)file_win2unix(strrep(c, [PlugPath, filesep], '')), PlugProc, 'UniformOutput', 0);
1324 end
1325 end
1326 PlugDesc(iPlug).Path = PlugPath;
1327 % Plugin installed: Managed by Brainstorm
1328 elseif isdir(PlugPath) && file_exist(bst_fullfile(PlugPath, 'plugin.mat'))
1329 iPlug = length(PlugDesc) + 1;
1330 PlugDesc(iPlug) = SearchPlugs(iSearch);
1331 PlugDesc(iPlug).Path = PlugPath;
1332 PlugDesc(iPlug).isLoaded = 0;
1333 PlugDesc(iPlug).isManaged = 1;
1334 % Plugin installed: Custom path
1335 elseif isfield(PluginCustomPath, PlugName) && ~isempty(PluginCustomPath.(PlugName)) && file_exist(PluginCustomPath.(PlugName))
1336 iPlug = length(PlugDesc) + 1;
1337 PlugDesc(iPlug) = SearchPlugs(iSearch);
1338 PlugDesc(iPlug).Path = PluginCustomPath.(PlugName);
1339 PlugDesc(iPlug).isLoaded = 0;
1340 PlugDesc(iPlug).isManaged = 0;
1341 end
1342 end
1343
1344 % === LOOK FOR UNREFERENCED PLUGINS ===
1345 % Compiled: do not look for unreferenced plugins
1346 if isCompiled
1347 PlugList = [];
1348 % Get a specific unlisted plugin
1349 elseif ~isempty(SelPlug)
1350 % Get plugin name
1351 if ischar(SelPlug)
1352 PlugName = lower(SelPlug);
1353 else
1354 PlugName = SelPlug.Name;
1355 end
1356 % If plugin is already referenced: skip
1357 if ismember(PlugName, {PlugDesc.Name})
1358 PlugList = [];
1359 % Else: Try to get target plugin as unreferenced
1360 else
1361 PlugList = struct('name', PlugName);
1362 end
1363 % Get all folders in Brainstorm plugins folder
1364 else
1365 PlugList = dir(UserPluginsDir);
1366 end
1367 % Process folders containing a plugin.mat file
1368 for iDir = 1:length(PlugList)
1369 % Ignore entry if plugin name is already in list of documented plugins
1370 PlugName = PlugList(iDir).name;
1371 if ismember(PlugName, {PlugDesc.Name})
1372 continue;
1373 end
1374 % Process only folders
1375 PlugDir = bst_fullfile(UserPluginsDir, PlugName);
1376 if ~isdir(PlugDir) || (PlugName(1) == '.')
1377 continue;
1378 end
1379 % Process only folders containing a 'plugin.mat' file
1380 PlugMatFile = bst_fullfile(PlugDir, 'plugin.mat');
1381 if ~file_exist(PlugMatFile)
1382 continue;
1383 end
1384 % If selecting only one plugin
1385 if ~isempty(SelPlug) && ischar(SelPlug) && ~strcmpi(PlugName, SelPlug)
1386 continue;
1387 end
1388 % Add plugin to list
1389 iPlug = length(PlugDesc) + 1;
1390 PlugDesc(iPlug) = GetStruct(PlugList(iDir).name);
1391 PlugDesc(iPlug).Path = PlugDir;
1392 PlugDesc(iPlug).isManaged = 1;
1393 PlugDesc(iPlug).isLoaded = ismember(PlugDir, matlabPath);
1394 end
1395
1396 % === READ PLUGIN.MAT ===
1397 for iPlug = 1:length(PlugDesc)
1398 % Try to load the plugin.mat file in the plugin folder
1399 PlugMatFile = bst_fullfile(PlugDesc(iPlug).Path, 'plugin.mat');
1400 if file_exist(PlugMatFile)
1401 try
1402 PlugMat = load(PlugMatFile);
1403 catch
1404 PlugMat = struct();
1405 end
1406 % Copy fields
1407 excludedFields = {'Name', 'Path', 'isLoaded', 'isManaged', 'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn'};
1408 loadFields = setdiff(fieldnames(db_template('PlugDesc')), excludedFields);
1409 for iField = 1:length(loadFields)
1410 if isfield(PlugMat, loadFields{iField}) && ~isempty(PlugMat.(loadFields{iField}))
1411 PlugDesc(iPlug).(loadFields{iField}) = PlugMat.(loadFields{iField});
1412 end
1413 end
1414 else
1415 PlugDesc(iPlug).URLzip = [];
1416 end
1417 end
1418 end
1419
1420
1421 %% ===== GET LOADED PLUGINS =====
1422 % USAGE: [PlugDesc, SearchPlugs] = bst_plugin('GetLoaded')
1423 function PlugDesc = GetLoaded()
1424 PlugDesc = GetInstalled();
1425 PlugDesc = PlugDesc([PlugDesc.isLoaded] == 1);
1426 end
1427
1428
1429 %% ===== GET DESCRIPTION =====
1430 % USAGE: [PlugDesc, errMsg] = GetDescription(PlugName/PlugDesc)
1431 function [PlugDesc, errMsg] = GetDescription(PlugName)
1432 % Initialize returned values
1433 errMsg = '';
1434 PlugDesc = [];
1435 % CALL: GetDescription(PlugDesc)
1436 if isstruct(PlugName)
1437 % Add the missing fields
1438 PlugDesc = struct_copy_fields(PlugName, db_template('PlugDesc'), 0);
1439 % CALL: GetDescription(PlugName)
1440 elseif ischar(PlugName)
1441 % Get supported plugins
1442 AllPlugs = GetSupported();
1443 % Find plugin in supported plugins
1444 iPlug = find(strcmpi({AllPlugs.Name}, PlugName));
1445 if isempty(iPlug)
1446 errMsg = ['Unknown plugin: ' PlugName];
1447 return;
1448 end
1449 % Return found plugin
1450 PlugDesc = AllPlugs(iPlug);
1451 else
1452 errMsg = 'Invalid call to GetDescription().';
1453 end
1454 end
1455
1456
1457 %% ===== GET TEST FILE PATH =====
1458 function TestFilePath = GetTestFilePath(PlugDesc)
1459 % If a test file is defined
1460 if ~isempty(PlugDesc.TestFile)
1461 % Try to find the test function in the path
1462 whichTest = which(PlugDesc.TestFile);
1463 % If it was found: use the parent folder
1464 if ~isempty(whichTest)
1465 % Get the test file path
1466 TestFilePath = bst_fileparts(whichTest);
1467 % FieldTrip: Ignore if found embedded in SPM12
1468 if strcmpi(PlugDesc.Name, 'fieldtrip')
1469 p = which('spm.m');
1470 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1471 TestFilePath = [];
1472 end
1473 % SPM12: Ignore if found embedded in ROAST or in FieldTrip
1474 elseif strcmpi(PlugDesc.Name, 'spm12')
1475 p = which('roast.m');
1476 q = which('ft_defaults.m');
1477 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1478 TestFilePath = [];
1479 end
1480 % Iso2mesh: Ignore if found embedded in ROAST
1481 elseif strcmpi(PlugDesc.Name, 'iso2mesh')
1482 p = which('roast.m');
1483 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1484 TestFilePath = [];
1485 end
1486 % jsonlab and jsnirfy: Ignore if found embedded in iso2mesh
1487 elseif strcmpi(PlugDesc.Name, 'jsonlab') || strcmpi(PlugDesc.Name, 'jsnirfy')
1488 p = which('iso2meshver.m');
1489 if ~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')
1490 TestFilePath = [];
1491 end
1492 % easyh5: Ignore if found embedded in iso2mesh or jsonlab
1493 elseif strcmpi(PlugDesc.Name, 'easyh5')
1494 p = which('iso2meshver.m');
1495 q = which('savejson.m');
1496 if (~isempty(p) && strMatchEdge(TestFilePath, bst_fileparts(p), 'start')) || (~isempty(q) && strMatchEdge(TestFilePath, bst_fileparts(q), 'start'))
1497 TestFilePath = [];
1498 end
1499 end
1500 else
1501 TestFilePath = [];
1502 end
1503 else
1504 TestFilePath = [];
1505 end
1506 end
1507
1508
1509 %% ===== GET README FILE ====
1510 % Get full path to the readme file
1511 function ReadmeFile = GetReadmeFile(PlugDesc)
1512 ReadmeFile = [];
1513 % If readme file is defined in the plugin structure
1514 if ~isempty(PlugDesc.ReadmeFile)
1515 % If full path already set: use it
1516 if file_exist(PlugDesc.ReadmeFile)
1517 ReadmeFile = PlugDesc.ReadmeFile;
1518 % Else: check in the plugin Path/SubFolder
1519 else
1520 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.ReadmeFile);
1521 if file_exist(tmpFile)
1522 ReadmeFile = tmpFile;
1523 elseif ~isempty(PlugDesc.SubFolder)
1524 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.ReadmeFile);
1525 if file_exist(tmpFile)
1526 ReadmeFile = tmpFile;
1527 end
1528 end
1529 end
1530 end
1531 % Search for default readme
1532 if isempty(ReadmeFile)
1533 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_readme.txt']);
1534 if file_exist(tmpFile)
1535 ReadmeFile = tmpFile;
1536 end
1537 end
1538 end
1539
1540
1541 %% ===== GET LOGO FILE ====
1542 % Get full path to the logo file
1543 function LogoFile = GetLogoFile(PlugDesc)
1544 LogoFile = [];
1545 % If logo file is defined in the plugin structure
1546 if ~isempty(PlugDesc.LogoFile)
1547 % If full path already set: use it
1548 if file_exist(PlugDesc.LogoFile)
1549 LogoFile = PlugDesc.LogoFile;
1550 % Else: check in the plugin Path/SubFolder
1551 else
1552 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.LogoFile);
1553 if file_exist(tmpFile)
1554 LogoFile = tmpFile;
1555 elseif ~isempty(PlugDesc.SubFolder)
1556 tmpFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.LogoFile);
1557 if file_exist(tmpFile)
1558 LogoFile = tmpFile;
1559 end
1560 end
1561 end
1562 end
1563 % Search for default logo
1564 if isempty(LogoFile)
1565 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.gif']);
1566 if file_exist(tmpFile)
1567 LogoFile = tmpFile;
1568 end
1569 end
1570 if isempty(LogoFile)
1571 tmpFile = bst_fullfile(bst_get('BrainstormDocDir'), 'plugins', [PlugDesc.Name '_logo.png']);
1572 if file_exist(tmpFile)
1573 LogoFile = tmpFile;
1574 end
1575 end
1576 end
1577
1578
1579 %% ===== INSTALL =====
1580 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('Install', PlugName, isInteractive=0, minVersion=[])
1581 function [isOk, errMsg, PlugDesc] = Install(PlugName, isInteractive, minVersion)
1582 % Returned variables
1583 isOk = 0;
1584 % Parse inputs
1585 if (nargin < 3) || isempty(minVersion)
1586 minVersion = [];
1587 elseif isnumeric(minVersion)
1588 minVersion = num2str(minVersion);
1589 end
1590 if (nargin < 2) || isempty(isInteractive)
1591 isInteractive = 0;
1592 end
1593 if ~ischar(PlugName)
1594 errMsg = 'Invalid call to Install()';
1595 PlugDesc = [];
1596 return;
1597 end
1598 % Backup calling progress bar;
1599 isCallBar = bst_progress('isvisible');
1600 if isCallBar
1601 pBarParams = bst_progress('getbarparams');
1602 end
1603 % Get plugin structure from name
1604 [PlugDesc, errMsg] = GetDescription(PlugName);
1605 if ~isempty(errMsg)
1606 return;
1607 end
1608 % Check if plugin is supported on Apple silicon
1609 OsType = bst_get('OsType', 0);
1610 if strcmpi(OsType, 'mac64arm') && ismember(PlugName, PluginsNotSupportAppleSilicon())
1611 errMsg = ['Plugin ', PlugName ' is not supported on Apple silicon yet.'];
1612 PlugDesc = [];
1613 return;
1614 end
1615 % Check if there is a URL to download
1616 if isempty(PlugDesc.URLzip)
1617 errMsg = ['No download URL for ', OsType, ': ', PlugName ''];
1618 return;
1619 end
1620 % Compiled version
1621 isCompiled = bst_iscompiled();
1622 if isCompiled && (PlugDesc.CompiledStatus == 0)
1623 errMsg = ['Plugin ', PlugName ' is not available in the compiled version of Brainstorm.'];
1624 return;
1625 end
1626 % Minimum Matlab version
1627 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
1628 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
1629 errMsg = ['Plugin ', PlugName ' is not supported for versions of Matlab <= ' strMinVer];
1630 return;
1631 end
1632 % Get online update (use existing cache)
1633 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugDesc.URLzip, 1);
1634 if ~isempty(newVersion)
1635 PlugDesc.Version = newVersion;
1636 end
1637 if ~isempty(newURLzip)
1638 PlugDesc.URLzip = newURLzip;
1639 end
1640
1641 % === PROCESS DEPENDENCIES ===
1642 % Check required plugins
1643 if ~isempty(PlugDesc.RequiredPlugs)
1644 bst_progress('text', ['Processing dependencies for ' PlugName '...']);
1645 disp(['BST> Processing dependencies: ' PlugName ' requires: ' sprintf('%s ', PlugDesc.RequiredPlugs{:,1})]);
1646 % Get the list of plugins that need to be installed
1647 installPlugs = {};
1648 installVer = {};
1649 strInstall = '';
1650 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
1651 PlugCheck = GetInstalled(PlugDesc.RequiredPlugs{iPlug,1});
1652 % Plugin not install: Install it
1653 if isempty(PlugCheck)
1654 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1655 installVer{end+1} = [];
1656 strInstall = [strInstall, '<B>' installPlugs{end} '</B> '];
1657 % Plugin installed: check version
1658 elseif (size(PlugDesc.RequiredPlugs,2) == 2)
1659 minVerDep = PlugDesc.RequiredPlugs{iPlug,2};
1660 if ~isempty(minVerDep) && (CompareVersions(minVerDep, PlugCheck.Version) > 0)
1661 installPlugs{end+1} = PlugDesc.RequiredPlugs{iPlug,1};
1662 installVer{end+1} = PlugDesc.RequiredPlugs{iPlug,2};
1663 strInstall = [strInstall, '<B>' installPlugs{end} '</B>(' installVer{end} ') '];
1664 end
1665 end
1666 end
1667 % If there are plugins to install
1668 if ~isempty(installPlugs)
1669 if isInteractive
1670 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> requires: ' strInstall ...
1671 '<BR><BR>Brainstorm will now install these plugins.' 10 10], 'Plugin manager');
1672 end
1673 for iPlug = 1:length(installPlugs)
1674 [isInstalled, errMsg] = Install(installPlugs{iPlug}, isInteractive, installVer{iPlug});
1675 if ~isInstalled
1676 errMsg = ['Error processing dependency: ' PlugDesc.RequiredPlugs{iPlug,1} 10 errMsg];
1677 return;
1678 end
1679 end
1680 end
1681 end
1682
1683 % === UPDATE: CHECK PREVIOUS INSTALL ===
1684 % Check if installed
1685 OldPlugDesc = GetInstalled(PlugName);
1686 % If already installed
1687 if ~isempty(OldPlugDesc)
1688 % If the plugin is not managed by Brainstorm: do not check versions
1689 if ~OldPlugDesc.isManaged
1690 isUpdate = 0;
1691 % If the requested version is higher
1692 elseif ~isempty(minVersion) && (CompareVersions(minVersion, OldPlugDesc.Version) > 0)
1693 isUpdate = 1;
1694 strUpdate = ['the installed version is outdated.<BR>Minimum version required: <I>' minVersion '</I>'];
1695 % If an update is available and auto-updates are requested
1696 elseif (PlugDesc.AutoUpdate == 1) && bst_get('AutoUpdates') && ... % If updates are enabled
1697 ((isGithubMaster(PlugDesc.URLzip) && ~strcmpi(PlugDesc.Version, OldPlugDesc.Version)) || ... % GitHub-master: update if different commit SHA strings
1698 (~isGithubMaster(PlugDesc.URLzip) && (CompareVersions(PlugDesc.Version, OldPlugDesc.Version) > 0))) % Regular stable version: update if online version is newer
1699 isUpdate = 1;
1700 strUpdate = 'an update is available online.';
1701 else
1702 isUpdate = 0;
1703 end
1704 % Update plugin
1705 if isUpdate
1706 % Compare versions
1707 strCompare = ['<FONT color="#707070">' ...
1708 'Old version : <I>' OldPlugDesc.Version '</I><BR>' ...
1709 'New version : <I>' PlugDesc.Version '</I></FONT><BR><BR>'];
1710 % Ask user for updating
1711 if isInteractive
1712 isConfirm = java_dialog('confirm', ...
1713 ['<HTML>Plugin <B>' PlugName '</B>: ' strUpdate '<BR>' ...
1714 'Download and install the latest version?<BR><BR>' strCompare], 'Plugin manager');
1715 % If update not confirmed: simply load the existing plugin
1716 if ~isConfirm
1717 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1718 return;
1719 end
1720 end
1721 disp(['BST> Plugin ' PlugName ' is outdated and will be updated.']);
1722 % Uninstall existing plugin
1723 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
1724 if ~isOk
1725 errMsg = ['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10];
1726 return;
1727 end
1728
1729 % No update: Load existing plugin and return
1730 else
1731 % Load plugin
1732 if ~OldPlugDesc.isLoaded
1733 [isLoaded, errMsg, PlugDesc] = Load(OldPlugDesc);
1734 if ~isLoaded
1735 errMsg = ['Could not load plugin ' PlugName ':' 10 errMsg];
1736 return;
1737 end
1738 else
1739 disp(['BST> Plugin ' PlugName ' already loaded: ' OldPlugDesc.Path]);
1740 end
1741 % Return old plugin
1742 PlugDesc = OldPlugDesc;
1743 isOk = 1;
1744 return;
1745 end
1746 else
1747 % Get user confirmation
1748 if isInteractive
1749 if ~isempty(PlugDesc.Version) && ~isequal(PlugDesc.Version, 'github-master') && ~isequal(PlugDesc.Version, 'latest')
1750 strVer = ['<FONT color="#707070">Latest version: ' PlugDesc.Version '</FONT><BR><BR>'];
1751 else
1752 strVer = '';
1753 end
1754 isConfirm = java_dialog('confirm', ...
1755 ['<HTML>Plugin <B>' PlugName '</B> is not installed on your computer.<BR>' ...
1756 '<B>Download</B> the latest version of ' PlugName ' now?<BR><BR>' ...
1757 strVer, ...
1758 '<FONT color="#707070">If this program is available on your computer,<BR>' ...
1759 'cancel this installation and use the menu: Plugins > <BR>' ...
1760 PlugName ' > Custom install > Set installation folder.</FONT><BR><BR>'], 'Plugin manager');
1761 if ~isConfirm
1762 errMsg = 'Installation aborted by user.';
1763 return;
1764 end
1765 end
1766 end
1767
1768 % === INSTALL PLUGIN ===
1769 bst_progress('text', ['Installing plugin ' PlugName '...']);
1770 % Managed plugin folder
1771 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
1772 % Delete existing folder
1773 if isdir(PlugPath)
1774 file_delete(PlugPath, 1, 3);
1775 end
1776 % Create folder
1777 if ~isdir(PlugPath)
1778 res = mkdir(PlugPath);
1779 if ~res
1780 errMsg = ['Error: Cannot create folder' 10 PlugPath];
1781 return
1782 end
1783 end
1784 % Setting progressbar image
1785 LogoFile = GetLogoFile(PlugDesc);
1786 if ~isempty(LogoFile)
1787 bst_progress('setimage', LogoFile);
1788 end
1789 % Get package file format
1790 if strcmpi(PlugDesc.URLzip(end-3:end), '.zip')
1791 pkgFormat = 'zip';
1792 elseif strcmpi(PlugDesc.URLzip(end-6:end), '.tar.gz') || strcmpi(PlugDesc.URLzip(end-3:end), '.tgz')
1793 pkgFormat = 'tgz';
1794 else
1795 disp('BST> Could not guess file format, trying ZIP...');
1796 pkgFormat = 'zip';
1797 end
1798 % Download file
1799 pkgFile = bst_fullfile(PlugPath, ['plugin.' pkgFormat]);
1800 disp(['BST> Downloading URL : ' PlugDesc.URLzip]);
1801 disp(['BST> Saving to file : ' pkgFile]);
1802 errMsg = gui_brainstorm('DownloadFile', PlugDesc.URLzip, pkgFile, ['Download plugin: ' PlugName], LogoFile);
1803 % If file was not downloaded correctly
1804 if ~isempty(errMsg)
1805 errMsg = ['Impossible to download ' PlugName ' automatically:' 10 errMsg];
1806 if ~isCompiled
1807 errMsg = [errMsg 10 10 ...
1808 'Alternative download solution:' 10 ...
1809 '1) Copy the URL below from the Matlab command window: ' 10 ...
1810 ' ' PlugDesc.URLzip 10 ...
1811 '2) Paste it in a web browser' 10 ...
1812 '3) Save the file and unzip it' 10 ...
1813 '4) Add to the Matlab path the folder containing ' PlugDesc.TestFile '.'];
1814 end
1815 bst_progress('removeimage');
1816 return;
1817 end
1818 % Update progress bar
1819 bst_progress('text', ['Installing plugin: ' PlugName '...']);
1820 if ~isempty(LogoFile)
1821 bst_progress('setimage', LogoFile);
1822 end
1823 % Unzip file
1824 switch (pkgFormat)
1825 case 'zip'
1826 bst_unzip(pkgFile, PlugPath);
1827 case 'tgz'
1828 if ispc
1829 untar(pkgFile, PlugPath);
1830 else
1831 curdir = pwd;
1832 cd(PlugPath);
1833 system(['tar -xf ' pkgFile]);
1834 cd(curdir);
1835 end
1836 end
1837 file_delete(pkgFile, 1, 3);
1838
1839 % === SAVE PLUGIN.MAT ===
1840 PlugDesc.Path = PlugPath;
1841 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
1842 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
1843 PlugDescSave = rmfield(PlugDesc, excludedFields);
1844 bst_save(PlugMatFile, PlugDescSave, 'v6');
1845
1846 % === CALLBACK: POST-DOWNLOADED ===
1847 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'DownloadedFcn');
1848 if ~isOk
1849 return;
1850 end
1851
1852 % === LOAD PLUGIN ===
1853 % Load plugin
1854 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
1855 if ~isOk
1856 bst_progress('removeimage');
1857 return;
1858 end
1859 % Update plugin description after first load, and delete unwanted files
1860 [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, 1);
1861 if ~isOk
1862 return;
1863 end
1864
1865 % === SHOW PLUGIN INFO ===
1866 % Log install
1867 bst_webread(['https://neuroimage.usc.edu/bst/pluglog.php?c=K8Yda7B&plugname=' PlugDesc.Name '&action=install']);
1868 % Show plugin information (interactive mode only)
1869 if isInteractive
1870 % Hide progress bar
1871 isProgress = bst_progress('isVisible');
1872 if isProgress
1873 bst_progress('hide');
1874 end
1875 % Message box: aknowledgements
1876 java_dialog('msgbox', ['<HTML>Plugin <B>' PlugName '</B> was sucessfully installed.<BR><BR>' ...
1877 'This software is not distributed by the Brainstorm developers.<BR>' ...
1878 'Please take a few minutes to read the license information,<BR>' ...
1879 'check the authors'' website and register online if recommended.<BR><BR>' ...
1880 '<B>Cite the authors</B> in your publications if you are using their software.<BR><BR>'], 'Plugin manager');
1881 % Show the readme file
1882 if ~isempty(PlugDesc.ReadmeFile)
1883 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
1884 end
1885 % Open the website
1886 if ~isempty(PlugDesc.URLinfo)
1887 web(PlugDesc.URLinfo, '-browser')
1888 end
1889 % Restore progress bar
1890 if isProgress
1891 bst_progress('show');
1892 end
1893 end
1894 % Remove logo
1895 bst_progress('removeimage');
1896 % Return success
1897 isOk = 1;
1898 % Restore calling progress bar
1899 if isCallBar
1900 bst_progress('setbarparams', pBarParams);
1901 end
1902 end
1903
1904
1905 %% ===== UPDATE DESCRIPTION =====
1906 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('UpdateDescription', PlugDesc, doDelete=0)
1907 function [isOk, errMsg, PlugDesc] = UpdateDescription(PlugDesc, doDelete)
1908 isOk = 1;
1909 errMsg = '';
1910 PlugPath = PlugDesc.Path;
1911 PlugName = PlugDesc.Name;
1912
1913 if nargin < 2
1914 doDelete = 0;
1915 end
1916
1917 % Plug in needs to be installed
1918 if isempty(bst_plugin('GetInstalled', PlugDesc.Name))
1919 isOk = 0;
1920 errMsg = ['Cannot update description, plugin ''' PlugDesc.Name ''' needs to be installed'];
1921 return
1922 end
1923
1924 % === DELETE UNWANTED FILES ===
1925 if doDelete && ~isempty(PlugDesc.DeleteFiles) && iscell(PlugDesc.DeleteFiles)
1926 warning('off', 'MATLAB:RMDIR:RemovedFromPath');
1927 for iDel = 1:length(PlugDesc.DeleteFiles)
1928 if ~isempty(PlugDesc.SubFolder)
1929 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, PlugDesc.DeleteFiles{iDel});
1930 else
1931 fileDel = bst_fullfile(PlugDesc.Path, PlugDesc.DeleteFiles{iDel});
1932 end
1933 if file_exist(fileDel)
1934 try
1935 file_delete(fileDel, 1, 3);
1936 catch
1937 disp(['BST> Plugin ' PlugName ': Could not delete file: ' PlugDesc.DeleteFiles{iDel}]);
1938 end
1939 else
1940 disp(['BST> Plugin ' PlugName ': Missing file: ' PlugDesc.DeleteFiles{iDel}]);
1941 end
1942 end
1943 warning('on', 'MATLAB:RMDIR:RemovedFromPath');
1944 end
1945
1946 % === SEARCH PROCESSES ===
1947 % Look for process_* functions in the process folder
1948 PlugProc = file_find(PlugPath, 'process_*.m', Inf, 0);
1949 if ~isempty(PlugProc)
1950 % Remove absolute path: use only path relative to the plugin Path
1951 PlugDesc.Processes = cellfun(@(c)file_win2unix(strrep(c, [PlugPath, filesep], '')), PlugProc, 'UniformOutput', 0);
1952 end
1953
1954 % === SAVE PLUGIN.MAT ===
1955 % Save installation date
1956 c = clock();
1957 PlugDesc.InstallDate = datestr(datenum(c(1), c(2), c(3), c(4), c(5), c(6)), 'dd-mmm-yyyy HH:MM:SS');
1958 % Get readme and logo
1959 PlugDesc.ReadmeFile = GetReadmeFile(PlugDesc);
1960 PlugDesc.LogoFile = GetLogoFile(PlugDesc);
1961 % Update plugin.mat
1962 excludedFields = {'LoadedFcn', 'UnloadedFcn', 'DownloadedFcn', 'InstalledFcn', 'UninstalledFcn', 'Path', 'isLoaded', 'isManaged'};
1963 PlugDescSave = rmfield(PlugDesc, excludedFields);
1964 PlugMatFile = bst_fullfile(PlugDesc.Path, 'plugin.mat');
1965 bst_save(PlugMatFile, PlugDescSave, 'v6');
1966
1967 % === CALLBACK: POST-INSTALL ===
1968 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'InstalledFcn');
1969 if ~isOk
1970 return;
1971 end
1972
1973 % === GET INSTALLED VERSION ===
1974 % Get installed version
1975 if ~isempty(PlugDesc.GetVersionFcn)
1976 testVer = [];
1977 try
1978 if ischar(PlugDesc.GetVersionFcn)
1979 testVer = eval(PlugDesc.GetVersionFcn);
1980 elseif isa(PlugDesc.GetVersionFcn, 'function_handle')
1981 testVer = feval(PlugDesc.GetVersionFcn);
1982 end
1983 catch
1984 disp(['BST> Could not get installed version with callback: ' PlugDesc.GetVersionFcn]);
1985 end
1986 if ~isempty(testVer)
1987 PlugDesc.Version = testVer;
1988 % Update plugin.mat
1989 PlugDescSave.Version = testVer;
1990 bst_save(PlugMatFile, PlugDescSave, 'v6');
1991 end
1992 end
1993 end
1994
1995 %% ===== INSTALL INTERACTIVE =====
1996 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallInteractive', PlugName)
1997 function [isOk, errMsg, PlugDesc] = InstallInteractive(PlugName)
1998 % Open progress bar
1999 isProgress = bst_progress('isVisible');
2000 if ~isProgress
2001 bst_progress('start', 'Plugin manager', 'Initialization...');
2002 end
2003 % Call silent function
2004 [isOk, errMsg, PlugDesc] = Install(PlugName, 1);
2005 % Handle errors
2006 if ~isOk
2007 bst_error(['Installation error:' 10 10 errMsg 10], 'Plugin manager', 0);
2008 elseif ~isempty(errMsg)
2009 java_dialog('msgbox', ['Installation message:' 10 10 errMsg 10], 'Plugin manager');
2010 end
2011 % Close progress bar
2012 if ~isProgress
2013 bst_progress('stop');
2014 end
2015 end
2016
2017
2018 %% ===== INSTALL MULTIPLE CHOICE =====
2019 % If multiple plugins provide the same functions (eg. FieldTrip and SPM): make sure at least one is installed
2020 % USAGE: [isOk, errMsg, PlugDesc] = bst_plugin('InstallMultipleChoice', PlugNames, isInteractive)
2021 function [isOk, errMsg, PlugDesc] = InstallMultipleChoice(PlugNames, isInteractive)
2022 % Check if one of the plugins is loaded
2023 for iPlug = 1:length(PlugNames)
2024 PlugInst = GetInstalled(PlugNames{iPlug});
2025 if ~isempty(PlugInst)
2026 [isOk, errMsg, PlugDesc] = Load(PlugNames{iPlug});
2027 if isOk
2028 return;
2029 end
2030 end
2031 end
2032 % If no plugin is loaded: Install the first in the list
2033 [isOk, errMsg, PlugDesc] = Install(PlugNames{1}, isInteractive);
2034 end
2035
2036
2037 %% ===== UNINSTALL =====
2038 % USAGE: [isOk, errMsg] = bst_plugin('Uninstall', PlugName, isInteractive=0, isDependencies=1)
2039 function [isOk, errMsg] = Uninstall(PlugName, isInteractive, isDependencies)
2040 % Returned variables
2041 isOk = 0;
2042 errMsg = '';
2043 % Parse inputs
2044 if (nargin < 3) || isempty(isDependencies)
2045 isDependencies = 1;
2046 end
2047 if (nargin < 2) || isempty(isInteractive)
2048 isInteractive = 0;
2049 end
2050 if ~ischar(PlugName)
2051 errMsg = 'Invalid call to Uninstall()';
2052 return;
2053 end
2054
2055 % === CHECK INSTALLATION ===
2056 % Get installation
2057 PlugDesc = GetInstalled(PlugName);
2058 % External plugin
2059 if ~isempty(PlugDesc) && ~isequal(PlugDesc.isManaged, 1)
2060 errMsg = ['<HTML>Plugin <B>' PlugName '</B> is not managed by Brainstorm.' 10 'Delete folder manually:' 10 PlugDesc.Path];
2061 return;
2062 % Plugin not installed: check if folder exists
2063 elseif isempty(PlugDesc) || isempty(PlugDesc.Path)
2064 % Get plugin structure from name
2065 [PlugDesc, errMsg] = GetDescription(PlugName);
2066 if ~isempty(errMsg)
2067 return;
2068 end
2069 % Managed plugin folder
2070 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugName);
2071 else
2072 PlugPath = PlugDesc.Path;
2073 end
2074 % Plugin not installed
2075 if ~file_exist(PlugPath)
2076 errMsg = ['Plugin ' PlugName ' is not installed.'];
2077 return;
2078 end
2079
2080 % === USER CONFIRMATION ===
2081 if isInteractive
2082 isConfirm = java_dialog('confirm', ['<HTML>Delete permanently plugin <B>' PlugName '</B>?' 10 10 PlugPath 10 10], 'Plugin manager');
2083 if ~isConfirm
2084 errMsg = 'Uninstall aborted by user.';
2085 return;
2086 end
2087 end
2088
2089 % === PROCESS DEPENDENCIES ===
2090 % Uninstall dependent plugins
2091 if isDependencies
2092 AllPlugs = GetSupported();
2093 for iPlug = 1:length(AllPlugs)
2094 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2095 disp(['BST> Uninstalling dependent plugin: ' AllPlugs(iPlug).Name]);
2096 Uninstall(AllPlugs(iPlug).Name, isInteractive);
2097 end
2098 end
2099 end
2100
2101 % === UNLOAD ===
2102 if isequal(PlugDesc.isLoaded, 1)
2103 [isUnloaded, errMsgUnload] = Unload(PlugDesc);
2104 if ~isempty(errMsgUnload)
2105 disp(['BST> Error unloading plugin ' PlugName ': ' errMsgUnload]);
2106 end
2107 end
2108
2109 % === UNINSTALL ===
2110 disp(['BST> Deleting plugin ' PlugName ': ' PlugPath]);
2111 % Delete plugin folder
2112 isDeleted = file_delete(PlugPath, 1, 3);
2113 if (isDeleted ~= 1)
2114 errMsg = ['Could not delete plugin folder: ' 10 PlugPath 10 10 ...
2115 'There is probably a file in that folder that is currently ' 10 ...
2116 'loaded in Matlab, but that cannot be unloaded dynamically.' 10 10 ...
2117 'Brainstorm will now close Matlab.' 10 ...
2118 'Restart Matlab and install again the plugin.' 10 10];
2119 if isInteractive
2120 java_dialog('error', errMsg, 'Restart Matlab');
2121 else
2122 disp([10 10 'BST> ' errMsg]);
2123 end
2124 quit('force');
2125 end
2126
2127 % === CALLBACK: POST-UNINSTALL ===
2128 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UninstalledFcn');
2129 if ~isOk
2130 return;
2131 end
2132
2133 % Return success
2134 isOk = 1;
2135 end
2136
2137
2138 %% ===== UNINSTALL INTERACTIVE =====
2139 % USAGE: [isOk, errMsg] = bst_plugin('UninstallInteractive', PlugName)
2140 function [isOk, errMsg] = UninstallInteractive(PlugName)
2141 % Open progress bar
2142 isProgress = bst_progress('isVisible');
2143 if ~isProgress
2144 bst_progress('start', 'Plugin manager', 'Initialization...');
2145 end
2146 % Call silent function
2147 [isOk, errMsg] = Uninstall(PlugName, 1);
2148 % Handle errors
2149 if ~isOk
2150 bst_error(['An error occurred while uninstalling plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2151 elseif ~isempty(errMsg)
2152 java_dialog('msgbox', ['Uninstall message:' 10 10 errMsg 10], 'Plugin manager');
2153 end
2154 % Close progress bar
2155 if ~isProgress
2156 bst_progress('stop');
2157 end
2158 end
2159
2160
2161 %% ===== UPDATE INTERACTIVE =====
2162 % USAGE: [isOk, errMsg] = bst_plugin('UpdateInteractive', PlugName)
2163 function [isOk, errMsg] = UpdateInteractive(PlugName)
2164 % Open progress bar
2165 isProgress = bst_progress('isVisible');
2166 if ~isProgress
2167 bst_progress('start', 'Plugin manager', 'Initialization...');
2168 end
2169 % Get new plugin
2170 [PlugRef, errMsg] = GetDescription(PlugName);
2171 isOk = isempty(errMsg);
2172 % Get installed plugin
2173 if isOk
2174 PlugInst = GetInstalled(PlugName);
2175 if isempty(PlugInst) || ~PlugInst.isManaged
2176 isOk = 0;
2177 errMsg = ['Plugin ' PlugName ' is not installed or not managed by Brainstorm.'];
2178 end
2179 end
2180 % Get online update (use cache when available)
2181 [newVersion, newURLzip] = GetVersionOnline(PlugName, PlugRef.URLzip, 1);
2182 if ~isempty(newVersion)
2183 PlugRef.Version = newVersion;
2184 end
2185 if ~isempty(newURLzip)
2186 PlugRef.URLzip = newURLzip;
2187 end
2188 % User confirmation
2189 if isOk
2190 isOk = java_dialog('confirm', ['<HTML>Update plugin <B>' PlugName '</B> ?<BR><BR><FONT color="#707070">' ...
2191 'Old version : <I>' PlugInst.Version '</I><BR>' ...
2192 'New version : <I>' PlugRef.Version '</I><BR><BR></FONT>'], 'Plugin manager');
2193 if ~isOk
2194 errMsg = 'Update aborted by user.';
2195 end
2196 end
2197 % Uninstall old
2198 if isOk
2199 [isOk, errMsg] = Uninstall(PlugName, 0, 0);
2200 end
2201 % Install new
2202 if isOk
2203 [isOk, errMsg, PlugDesc] = Install(PlugName, 0);
2204 else
2205 PlugDesc = [];
2206 end
2207 % Handle errors
2208 if ~isOk
2209 bst_error(['An error occurred while updating plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
2210 elseif ~isempty(errMsg)
2211 java_dialog('msgbox', ['Update message:' 10 10 errMsg 10], 'Plugin manager');
2212 end
2213 % Close progress bar
2214 if ~isProgress
2215 bst_progress('stop');
2216 end
2217 % Plugin was updated successfully
2218 if ~isempty(PlugDesc)
2219 % Show the readme file
2220 if ~isempty(PlugDesc.ReadmeFile)
2221 view_text(PlugDesc.ReadmeFile, ['Installed plugin: ' PlugName], 1, 1);
2222 end
2223 % Open the website
2224 if ~isempty(PlugDesc.URLinfo)
2225 web(PlugDesc.URLinfo, '-browser')
2226 end
2227 end
2228 end
2229
2230
2231 %% ===== LOAD =====
2232 % USAGE: [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose=1)
2233 function [isOk, errMsg, PlugDesc] = Load(PlugDesc, isVerbose)
2234 % Parse inputs
2235 if (nargin < 2) || isempty(isVerbose)
2236 isVerbose = 1;
2237 end
2238 % Initialize returned variables
2239 isOk = 0;
2240 % Get plugin structure from name
2241 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2242 if ~isempty(errMsg)
2243 return;
2244 end
2245 % Check if plugin is supported on Apple silicon
2246 OsType = bst_get('OsType', 0);
2247 if strcmpi(OsType, 'mac64arm') && ismember(PlugDesc.Name, PluginsNotSupportAppleSilicon())
2248 errMsg = ['Plugin ', PlugDesc.Name ' is not supported on Apple silicon yet.'];
2249 return;
2250 end
2251 % Minimum Matlab version
2252 if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer)
2253 strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100));
2254 errMsg = ['Plugin ', PlugDesc.Name ' is not supported for versions of Matlab <= ' strMinVer];
2255 return;
2256 end
2257
2258 % === PROCESS DEPENDENCIES ===
2259 % Unload incompatible plugins
2260 if ~isempty(PlugDesc.UnloadPlugs)
2261 for iPlug = 1:length(PlugDesc.UnloadPlugs)
2262 % disp(['BST> Unloading incompatible plugin: ' PlugDesc.UnloadPlugs{iPlug}]);
2263 Unload(PlugDesc.UnloadPlugs{iPlug}, isVerbose);
2264 end
2265 end
2266
2267 % === ALREADY LOADED ===
2268 % If plugin is already full loaded
2269 if isequal(PlugDesc.isLoaded, 1) && ~isempty(PlugDesc.Path)
2270 if isVerbose
2271 errMsg = ['Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path];
2272 end
2273 return;
2274 end
2275 % Managed plugin path
2276 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2277 if file_exist(PlugPath)
2278 PlugDesc.isManaged = 1;
2279 % Custom installation
2280 else
2281 PluginCustomPath = bst_get('PluginCustomPath');
2282 if isfield(PluginCustomPath, PlugDesc.Name) && ~isempty(bst_fullfile(PluginCustomPath.(PlugDesc.Name))) && file_exist(bst_fullfile(PluginCustomPath.(PlugDesc.Name)))
2283 PlugPath = PluginCustomPath.(PlugDesc.Name);
2284 end
2285 PlugDesc.isManaged = 0;
2286 end
2287 % Managed install: Detect if there is a single subfolder containing all the files
2288 if PlugDesc.isManaged && ~isempty(PlugDesc.TestFile) && ~file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
2289 dirList = dir(PlugPath);
2290 for iDir = 1:length(dirList)
2291 % Not folder or . : skip
2292 if (dirList(iDir).name(1) == '.') || ~dirList(iDir).isdir
2293 continue;
2294 end
2295 % Check if test file is in the folder
2296 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.TestFile))
2297 PlugDesc.SubFolder = dirList(iDir).name;
2298 break;
2299 % Otherwise, check in any of the subfolders
2300 elseif ~isempty(PlugDesc.LoadFolders)
2301 for iSubDir = 1:length(PlugDesc.LoadFolders)
2302 if file_exist(bst_fullfile(PlugPath, dirList(iDir).name, PlugDesc.LoadFolders{iSubDir}, PlugDesc.TestFile))
2303 PlugDesc.SubFolder = dirList(iDir).name;
2304 break;
2305 end
2306 end
2307 end
2308 end
2309 end
2310 % Check if test function already available in the path
2311 TestFilePath = GetTestFilePath(PlugDesc);
2312 if ~isempty(TestFilePath)
2313 PlugDesc.isLoaded = 1;
2314 % Handle case symbolic link
2315 try
2316 PlugPath = builtin('_canonicalizepath', PlugPath);
2317 catch
2318 % Nothing here
2319 end
2320 PlugDesc.isManaged = strMatchEdge(which(PlugDesc.TestFile), PlugPath, 'start');
2321 if PlugDesc.isManaged
2322 PlugDesc.Path = PlugPath;
2323 else
2324 PlugDesc.Path = TestFilePath;
2325 end
2326 if isVerbose
2327 disp(['BST> Plugin ' PlugDesc.Name ' already loaded: ' PlugDesc.Path]);
2328 end
2329 isOk = 1;
2330 return;
2331 end
2332
2333 % === CHECK LOADABILITY ===
2334 PlugDesc.Path = PlugPath;
2335 if ~file_exist(PlugDesc.Path)
2336 errMsg = ['Plugin ' PlugDesc.Name ' not installed.' 10 'Missing folder: ' PlugDesc.Path];
2337 return;
2338 end
2339 % Set logo
2340 LogoFile = GetLogoFile(PlugDesc);
2341 if ~isempty(LogoFile)
2342 bst_progress('setimage', LogoFile);
2343 end
2344
2345 % Load required plugins
2346 if ~isempty(PlugDesc.RequiredPlugs)
2347 for iPlug = 1:size(PlugDesc.RequiredPlugs,1)
2348 % disp(['BST> Loading required plugin: ' PlugDesc.RequiredPlugs{iPlug,1}]);
2349 [isOk, errMsg] = Load(PlugDesc.RequiredPlugs{iPlug,1}, isVerbose);
2350 if ~isOk
2351 errMsg = ['Error processing dependencies: ', PlugDesc.Name, 10, errMsg];
2352 bst_progress('removeimage');
2353 return;
2354 end
2355 end
2356 end
2357
2358 % === LOAD PLUGIN ===
2359 % Add plugin folder to path
2360 if ~isempty(PlugDesc.SubFolder)
2361 PlugHomeDir = bst_fullfile(PlugPath, PlugDesc.SubFolder);
2362 else
2363 PlugHomeDir = PlugPath;
2364 end
2365 % Do not modify path in compiled mode
2366 isCompiled = bst_iscompiled();
2367 if ~isCompiled
2368 % Handle case symbolic link
2369 try
2370 PlugHomeDir = builtin('_canonicalizepath', PlugHomeDir);
2371 catch
2372 % Nothing here
2373 end
2374 addpath(PlugHomeDir);
2375 if isVerbose
2376 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ' PlugHomeDir]);
2377 end
2378 % Add specific subfolders to path
2379 if ~isempty(PlugDesc.LoadFolders)
2380 % Load all all subfolders
2381 if isequal(PlugDesc.LoadFolders, '*') || isequal(PlugDesc.LoadFolders, {'*'})
2382 if isVerbose
2383 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, '*']);
2384 end
2385 addpath(genpath(PlugHomeDir));
2386 % Load specific subfolders
2387 else
2388 for i = 1:length(PlugDesc.LoadFolders)
2389 subDir = PlugDesc.LoadFolders{i};
2390 if isequal(filesep, '\')
2391 subDir = strrep(subDir, '/', '\');
2392 end
2393 if ~isempty(dir([PlugHomeDir, filesep, subDir]))
2394 if isVerbose
2395 disp(['BST> Adding plugin ' PlugDesc.Name ' to path: ', PlugHomeDir, filesep, subDir]);
2396 end
2397 if regexp(subDir, '\*[/\\]*$')
2398 subDir = regexprep(subDir, '\*[/\\]*$', '');
2399 addpath(genpath([PlugHomeDir, filesep, subDir]));
2400 else
2401 addpath([PlugHomeDir, filesep, subDir]);
2402 end
2403 end
2404 end
2405 end
2406 end
2407 end
2408
2409 % === TEST FUNCTION ===
2410 % Check if test function is available on path
2411 if ~isCompiled && ~isempty(PlugDesc.TestFile) && (exist(PlugDesc.TestFile, 'file') == 0)
2412 errMsg = ['Plugin ' PlugDesc.Name ' successfully loaded from:' 10 PlugHomeDir 10 10 ...
2413 'However, the function ' PlugDesc.TestFile ' is not accessible in the Matlab path.' 10 10 ...
2414 'Try the following:' 10 ...
2415 '1. Update the plugin ' PlugDesc.Name 10 ...
2416 '2. If the issue persists, restart Matlab and Brainstorm.'];
2417 bst_progress('removeimage');
2418 return;
2419 end
2420
2421 % === CALLBACK: POST-LOAD ===
2422 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'LoadedFcn');
2423
2424 % Remove logo
2425 bst_progress('removeimage');
2426 % Return success
2427 PlugDesc.isLoaded = isOk;
2428 end
2429
2430
2431 %% ===== LOAD INTERACTIVE =====
2432 % USAGE: [isOk, errMsg, PlugDesc] = LoadInteractive(PlugName/PlugDesc)
2433 function [isOk, errMsg, PlugDesc] = LoadInteractive(PlugDesc)
2434 % Open progress bar
2435 isProgress = bst_progress('isVisible');
2436 if ~isProgress
2437 bst_progress('start', 'Plugin manager', 'Loading plugin...');
2438 end
2439 % Call silent function
2440 [isOk, errMsg, PlugDesc] = Load(PlugDesc);
2441 % Handle errors
2442 if ~isOk
2443 bst_error(['Load error:' 10 10 errMsg 10], 'Plugin manager', 0);
2444 elseif ~isempty(errMsg)
2445 java_dialog('msgbox', ['Load message:' 10 10 errMsg 10], 'Plugin manager');
2446 end
2447 % Close progress bar
2448 if ~isProgress
2449 bst_progress('stop');
2450 end
2451 end
2452
2453
2454 %% ===== UNLOAD =====
2455 % USAGE: [isOk, errMsg, PlugDesc] = Unload(PlugName/PlugDesc, isVerbose)
2456 function [isOk, errMsg, PlugDesc] = Unload(PlugDesc, isVerbose)
2457 % Parse inputs
2458 if (nargin < 2) || isempty(isVerbose)
2459 isVerbose = 1;
2460 end
2461 % Initialize returned variables
2462 isOk = 0;
2463 errMsg = '';
2464 % Get installation
2465 InstPlugDesc = GetInstalled(PlugDesc);
2466 % Plugin not installed: check if folder exists
2467 if isempty(InstPlugDesc) || isempty(InstPlugDesc.Path)
2468 % Get plugin structure from name
2469 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2470 if ~isempty(errMsg)
2471 return;
2472 end
2473 % Managed plugin folder
2474 PlugPath = bst_fullfile(bst_get('UserPluginsDir'), PlugDesc.Name);
2475 else
2476 PlugDesc = InstPlugDesc;
2477 PlugPath = PlugDesc.Path;
2478 end
2479 % Plugin not installed
2480 if ~file_exist(PlugPath)
2481 errMsg = ['Plugin ' PlugDesc.Name ' is not installed.' 10 'Missing folder: ' PlugPath];
2482 return;
2483 end
2484 % Get plugin structure from name
2485 [PlugDesc, errMsg] = GetDescription(PlugDesc);
2486 if ~isempty(errMsg)
2487 return;
2488 end
2489
2490 % === PROCESS DEPENDENCIES ===
2491 % Unload dependent plugins
2492 AllPlugs = GetSupported();
2493 for iPlug = 1:length(AllPlugs)
2494 if ~isempty(AllPlugs(iPlug).RequiredPlugs) && ismember(PlugDesc.Name, AllPlugs(iPlug).RequiredPlugs(:,1))
2495 Unload(AllPlugs(iPlug));
2496 end
2497 end
2498
2499 % === UNLOAD PLUGIN ===
2500 % Do not modify path in compiled mode
2501 if ~bst_iscompiled()
2502 matlabPath = str_split(path, pathsep);
2503 % Remove plugin folder and subfolders from path
2504 allSubFolders = str_split(genpath(PlugPath), pathsep);
2505 for i = 1:length(allSubFolders)
2506 if ismember(allSubFolders{i}, matlabPath)
2507 rmpath(allSubFolders{i});
2508 if isVerbose
2509 disp(['BST> Removing plugin ' PlugDesc.Name ' from path: ' allSubFolders{i}]);
2510 end
2511 end
2512 end
2513 end
2514
2515 % === TEST FUNCTION ===
2516 % Check if test function is still available on path
2517 if ~isempty(PlugDesc.TestFile) && ~isempty(which(PlugDesc.TestFile))
2518 errMsg = ['Plugin ' PlugDesc.Name ' successfully unloaded from: ' 10 PlugPath 10 10 ...
2519 'However, another version is still accessible on the Matlab path:' 10 which(PlugDesc.TestFile) 10 10 ...
2520 'Please remove this folder from the Matlab path.'];
2521 return;
2522 end
2523
2524 % === CALLBACK: POST-UNLOAD ===
2525 [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UnloadedFcn');
2526 if ~isOk
2527 return;
2528 end
2529
2530 % Return success
2531 PlugDesc.isLoaded = 0;
2532 isOk = 1;
2533 end
2534
2535
2536 %% ===== UNLOAD INTERACTIVE =====
2537 % USAGE: [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugName/PlugDesc)
2538 function [isOk, errMsg, PlugDesc] = UnloadInteractive(PlugDesc)
2539 % Open progress bar
2540 isProgress = bst_progress('isVisible');
2541 if ~isProgress
2542 bst_progress('start', 'Plugin manager', 'Unloading plugin...');
2543 end
2544 % Call silent function
2545 [isOk, errMsg, PlugDesc] = Unload(PlugDesc);
2546 % Handle errors
2547 if ~isOk
2548 bst_error(['Unload error:' 10 10 errMsg 10], 'Plugin manager', 0);
2549 elseif ~isempty(errMsg)
2550 java_dialog('msgbox', ['Unload message:' 10 10 errMsg 10], 'Plugin manager');
2551 end
2552 % Close progress bar
2553 if ~isProgress
2554 bst_progress('stop');
2555 end
2556 end
2557
2558
2559 %% ===== LIST =====
2560 % USAGE: strList = bst_plugin('List', Target='installed', isGui=0) % Target={'supported','installed', 'loaded'}
2561 function strList = List(Target, isGui)
2562 % Parse inputs
2563 if (nargin < 2) || isempty(isGui)
2564 isGui = 0;
2565 end
2566 if (nargin < 1) || isempty(Target)
2567 Target = 'Installed';
2568 else
2569 Target = [upper(Target(1)), lower(Target(2:end))];
2570 end
2571 % Get plugins to list
2572 strTitle = sprintf('%s plugins', Target);
2573 switch (Target)
2574 case 'Supported'
2575 PlugDesc = GetSupported();
2576 isInstalled = 0;
2577 case 'Installed'
2578 strTitle = [strTitle ' (*=Loaded)'];
2579 PlugDesc = GetInstalled();
2580 isInstalled = 1;
2581 case 'Loaded'
2582 PlugDesc = GetLoaded();
2583 isInstalled = 1;
2584 otherwise
2585 error(['Invalid target: ' Target]);
2586 end
2587 if isempty(PlugDesc)
2588 return;
2589 end
2590 % Sort by plugin names
2591 [tmp,I] = sort({PlugDesc.Name});
2592 PlugDesc = PlugDesc(I);
2593
2594 % Get Brainstorm info
2595 bstVer = bst_get('Version');
2596 bstDir = bst_get('BrainstormHomeDir');
2597 % Cut version string (short github SHA)
2598 if (length(bstVer.Commit) > 13)
2599 bstGit = ['git @', bstVer.Commit(1:7)];
2600 bstURL = ['https://github.com/brainstorm-tools/brainstorm3/archive/' bstVer.Commit '.zip'];
2601 structVer = bstGit;
2602 else
2603 bstGit = '';
2604 bstURL = '';
2605 structVer = bstVer.Version;
2606 end
2607
2608 % Max lengths
2609 headerName = ' Name';
2610 headerVersion = 'Version';
2611 headerPath = 'Install path';
2612 headerUrl = 'Downloaded from';
2613 headerDate = 'Install date';
2614 maxName = max(cellfun(@length, {PlugDesc.Name, headerName, 'brainstorm'}));
2615 maxVer = min(13, max(cellfun(@length, {PlugDesc.Version, headerVersion, bstGit})));
2616 maxUrl = max(cellfun(@length, {PlugDesc.URLzip, headerUrl, bstURL}));
2617 maxDate = 12;
2618 if isInstalled
2619 strDate = [' | ', headerDate, repmat(' ', 1, maxDate-length(headerDate))];
2620 strDateSep = ['-|-', repmat('-',1,maxDate)];
2621 maxPath = max(cellfun(@length, {PlugDesc.Path, headerPath}));
2622 strPath = [' | ', headerPath, repmat(' ', 1, maxPath-length(headerPath))];
2623 strPathSep = ['-|-', repmat('-',1,maxPath)];
2624 strBstVer = [' | ', bstVer.Date, repmat(' ', 1, maxDate-length(bstVer.Date))];
2625 strBstDir = [' | ', bstDir, repmat(' ', 1, maxPath-length(bstDir))];
2626 else
2627 strDate = '';
2628 strDateSep = '';
2629 strPath = '';
2630 strPathSep = '';
2631 strBstVer = '';
2632 strBstDir = '';
2633 end
2634 % Print column headers
2635 strList = [headerName, repmat(' ', 1, maxName-length(headerName) + 2) ...
2636 ' | ', headerVersion, repmat(' ', 1, maxVer-length(headerVersion)), ...
2637 strDate, strPath, ...
2638 ' | ' headerUrl 10 ...
2639 repmat('-',1,maxName + 2), '-|-', repmat('-',1,maxVer), strDateSep, strPathSep, '-|-', repmat('-',1,maxUrl) 10];
2640
2641 % Print Brainstorm information
2642 strList = [strList '* ', ...
2643 'brainstorm', repmat(' ', 1, maxName-length('brainstorm')) ...
2644 ' | ', bstGit, repmat(' ', 1, maxVer-length(bstGit)), ...
2645 strBstVer, strBstDir, ...
2646 ' | ' bstURL 10];
2647
2648 % Print installed plugins to standard output
2649 for iPlug = 1:length(PlugDesc)
2650 % Loaded plugin
2651 if PlugDesc(iPlug).isLoaded
2652 strLoaded = '* ';
2653 else
2654 strLoaded = ' ';
2655 end
2656 % Cut installation date: Only date, no time
2657 if (length(PlugDesc(iPlug).InstallDate) > 11)
2658 plugDate = PlugDesc(iPlug).InstallDate(1:11);
2659 else
2660 plugDate = PlugDesc(iPlug).InstallDate;
2661 end
2662 % Installed listing
2663 if isInstalled
2664 strDate = [' | ', plugDate, repmat(' ', 1, maxDate-length(plugDate))];
2665 strPath = [' | ', PlugDesc(iPlug).Path, repmat(' ', 1, maxPath-length(PlugDesc(iPlug).Path))];
2666 else
2667 strDate = '';
2668 strPath = '';
2669 end
2670 % Get installed version
2671 if (length(PlugDesc(iPlug).Version) > 13) % Cut version string (short github SHA)
2672 plugVer = ['git @', PlugDesc(iPlug).Version(1:7)];
2673 else
2674 plugVer = PlugDesc(iPlug).Version;
2675 end
2676 % Get installed version with GetVersionFcn
2677 if isempty(plugVer) && isfield(PlugDesc(iPlug),'GetVersionFcn') && ~isempty(PlugDesc(iPlug).GetVersionFcn)
2678 % Load plugin if needed
2679 tmpLoad = 0;
2680 if ~PlugDesc(iPlug).isLoaded
2681 tmpLoad = 1;
2682 Load(PlugDesc(iPlug), 0);
2683 end
2684 try
2685 if ischar(PlugDesc(iPlug).GetVersionFcn)
2686 plugVer = eval(PlugDesc(iPlug).GetVersionFcn);
2687 elseif isa(PlugDesc(iPlug).GetVersionFcn, 'function_handle')
2688 plugVer = feval(PlugDesc(iPlug).GetVersionFcn);
2689 end
2690 catch
2691 disp(['BST> Could not get installed version with callback: ' PlugDesc(iPlug).GetVersionFcn]);
2692 end
2693 % Unload plugin
2694 if tmpLoad
2695 Unload(PlugDesc(iPlug), 0);
2696 end
2697 end
2698 % Assemble plugin text row
2699 strList = [strList strLoaded, ...
2700 PlugDesc(iPlug).Name, repmat(' ', 1, maxName-length(PlugDesc(iPlug).Name)) ...
2701 ' | ', plugVer, repmat(' ', 1, maxVer-length(plugVer)), ...
2702 strDate, strPath, ...
2703 ' | ' PlugDesc(iPlug).URLzip 10];
2704 end
2705 % Display output
2706 if isGui
2707 view_text(strList, strTitle);
2708 % No string returned: display it in the command window
2709 elseif (nargout == 0)
2710 disp([10 strTitle 10 10 strList]);
2711 end
2712 end
2713
2714
2715 %% ===== MENUS: CREATE =====
2716 function j = MenuCreate(jMenu, jPlugsPrev, PlugDesc, fontSize)
2717 import org.brainstorm.icon.*;
2718 % Get all the supported plugins
2719 if isempty(PlugDesc)
2720 PlugDesc = GetSupported();
2721 end
2722 % Get Matlab version
2723 MatlabVersion = bst_get('MatlabVersion');
2724 isCompiled = bst_iscompiled();
2725 % Submenus array
2726 jSub = {};
2727 % Generate submenus array from existing menu
2728 if ~isCompiled && jMenu.getMenuComponentCount > 0
2729 for iItem = 0 : jMenu.getItemCount-1
2730 if ~isempty(regexp(jMenu.getMenuComponent(iItem).class, 'JMenu$', 'once'))
2731 jSub(end+1,1:2) = {char(jMenu.getMenuComponent(iItem).getText), jMenu.getMenuComponent(iItem)};
2732 end
2733 end
2734 end
2735 % Editing an existing menu?
2736 if isempty(jPlugsPrev)
2737 isNewMenu = 1;
2738 j = repmat(struct(), 0);
2739 else
2740 isNewMenu = 0;
2741 j = repmat(jPlugsPrev(1), 0);
2742 end
2743 % Process each plugin
2744 for iPlug = 1:length(PlugDesc)
2745 Plug = PlugDesc(iPlug);
2746 % Skip if Matlab is too old
2747 if ~isempty(Plug.MinMatlabVer) && (Plug.MinMatlabVer > 0) && (MatlabVersion < Plug.MinMatlabVer)
2748 continue;
2749 end
2750 % Skip if not supported in compiled version
2751 if isCompiled && (Plug.CompiledStatus == 0)
2752 continue;
2753 end
2754 % === Add menus for each plugin ===
2755 % One menu per plugin
2756 ij = length(j) + 1;
2757 j(ij).name = Plug.Name;
2758 % Skip if it is already a menu item
2759 if ~isNewMenu
2760 iPlugPrev = ismember({jPlugsPrev.name}, Plug.Name);
2761 if any(iPlugPrev)
2762 j(ij) = jPlugsPrev(iPlugPrev);
2763 continue
2764 end
2765 end
2766 % Category=submenu
2767 if ~isempty(Plug.Category)
2768 if isempty(jSub) || ~ismember(Plug.Category, jSub(:,1))
2769 jParent = gui_component('Menu', jMenu, [], Plug.Category, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2770 jSub(end+1,1:2) = {Plug.Category, jParent};
2771 else
2772 iSub = find(strcmpi(jSub(:,1), Plug.Category));
2773 jParent = jSub{iSub,2};
2774 end
2775 else
2776 jParent = jMenu;
2777 end
2778 % Compiled and included: Simple static menu
2779 if isCompiled && (Plug.CompiledStatus == 2)
2780 j(ij).menu = gui_component('MenuItem', jParent, [], Plug.Name, [], [], [], fontSize);
2781 % Do not create submenus for compiled version
2782 else
2783 % Main menu
2784 j(ij).menu = gui_component('Menu', jParent, [], Plug.Name, [], [], [], fontSize);
2785 % Version
2786 j(ij).version = gui_component('MenuItem', j(ij).menu, [], 'Version', [], [], [], fontSize);
2787 j(ij).versep = java_create('javax.swing.JSeparator');
2788 j(ij).menu.add(j(ij).versep);
2789 % Install
2790 j(ij).install = gui_component('MenuItem', j(ij).menu, [], 'Install', IconLoader.ICON_DOWNLOAD, [], @(h,ev)InstallInteractive(Plug.Name), fontSize);
2791 % Update
2792 j(ij).update = gui_component('MenuItem', j(ij).menu, [], 'Update', IconLoader.ICON_RELOAD, [], @(h,ev)UpdateInteractive(Plug.Name), fontSize);
2793 % Uninstall
2794 j(ij).uninstall = gui_component('MenuItem', j(ij).menu, [], 'Uninstall', IconLoader.ICON_DELETE, [], @(h,ev)UninstallInteractive(Plug.Name), fontSize);
2795 j(ij).menu.addSeparator();
2796 % Custom install
2797 j(ij).custom = gui_component('Menu', j(ij).menu, [], 'Custom install', IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2798 j(ij).customset = gui_component('MenuItem', j(ij).custom, [], 'Select installation folder', [], [], @(h,ev)SetCustomPath(Plug.Name), fontSize);
2799 j(ij).custompath = gui_component('MenuItem', j(ij).custom, [], 'Path not set', [], [], [], fontSize);
2800 j(ij).custompath.setEnabled(0);
2801 j(ij).custom.addSeparator();
2802 j(ij).customdel = gui_component('MenuItem', j(ij).custom, [], 'Ignore local installation', [], [], @(h,ev)SetCustomPath(Plug.Name, 0), fontSize);
2803 j(ij).menu.addSeparator();
2804 % Load
2805 j(ij).load = gui_component('MenuItem', j(ij).menu, [], 'Load', IconLoader.ICON_GOOD, [], @(h,ev)LoadInteractive(Plug.Name), fontSize);
2806 j(ij).unload = gui_component('MenuItem', j(ij).menu, [], 'Unload', IconLoader.ICON_BAD, [], @(h,ev)UnloadInteractive(Plug.Name), fontSize);
2807 j(ij).menu.addSeparator();
2808 % Website
2809 j(ij).web = gui_component('MenuItem', j(ij).menu, [], 'Website', IconLoader.ICON_EXPLORER, [], @(h,ev)web(Plug.URLinfo, '-browser'), fontSize);
2810 j(ij).usage = gui_component('MenuItem', j(ij).menu, [], 'Usage statistics', IconLoader.ICON_TS_DISPLAY, [], @(h,ev)bst_userstat(0,Plug.Name), fontSize);
2811 % Extra menus
2812 if ~isempty(Plug.ExtraMenus)
2813 j(ij).menu.addSeparator();
2814 for iMenu = 1:size(Plug.ExtraMenus,1)
2815 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);
2816 end
2817 end
2818 end
2819 end
2820 % === Remove menus for plugins with description ===
2821 if ~isempty(jPlugsPrev)
2822 [~, iOld] = setdiff({jPlugsPrev.name}, {PlugDesc.Name});
2823 for ix = 1 : length(iOld)
2824 % Find category menu component
2825 jMenuCat = jPlugsPrev(iOld(ix)).menu.getParent.getInvoker;
2826 % Find index in parent
2827 iDel = [];
2828 for ic = 0 : jMenuCat.getMenuComponentCount-1
2829 if jPlugsPrev(iOld(ix)).menu == jMenuCat.getMenuComponent(ic)
2830 iDel = ic;
2831 break
2832 end
2833 end
2834 % Remove from parent
2835 if ~isempty(iDel)
2836 jMenuCat.remove(iDel);
2837 end
2838 end
2839 end
2840 % Create options for adding user-defined plugins
2841 if ~isCompiled && isNewMenu
2842 menuCategory = 'User defined';
2843 jMenuUserDef = [];
2844 for iMenuItem = 0 : jMenu.getItemCount-1
2845 if ~isempty(regexp(jMenu.getMenuComponent(iMenuItem).class, 'JMenu$', 'once')) && strcmp(char(jMenu.getMenuComponent(iMenuItem).getText), menuCategory)
2846 jMenuUserDef = jMenu.getMenuComponent(iMenuItem);
2847 end
2848 end
2849 if isempty(jMenuUserDef)
2850 jMenuUserDef = gui_component('Menu', jMenu, [], menuCategory, IconLoader.ICON_FOLDER_OPEN, [], [], fontSize);
2851 end
2852 jAddUserDefMan = gui_component('MenuItem', [], [], 'Add manually', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('manual'), fontSize);
2853 jAddUserDefFile = gui_component('MenuItem', [], [], 'Add from file', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('file'), fontSize);
2854 jAddUserDefUrl = gui_component('MenuItem', [], [], 'Add from URL', IconLoader.ICON_EDIT, [], @(h,ev)AddUserDefDesc('url'), fontSize);
2855 jRmvUserDefMan = gui_component('MenuItem', [], [], 'Remove plugin', IconLoader.ICON_DELETE, [], @(h,ev)RemoveUserDefDesc, fontSize);
2856 % Insert "Add" options at the begining of the 'User defined' menu
2857 jMenuUserDef.insert(jAddUserDefMan, 0);
2858 jMenuUserDef.insert(jAddUserDefFile, 1);
2859 jMenuUserDef.insert(jAddUserDefUrl, 2);
2860 jMenuUserDef.insert(jRmvUserDefMan, 3);
2861 jMenuUserDef.insertSeparator(4);
2862 end
2863 % List
2864 if ~isCompiled && isNewMenu
2865 jMenu.addSeparator();
2866 gui_component('MenuItem', jMenu, [], 'List', IconLoader.ICON_EDIT, [], @(h,ev)List('Installed', 1), fontSize);
2867 end
2868 end
2869
2870
2871 %% ===== MENUS: UPDATE =====
2872 function MenuUpdate(jMenu, fontSize)
2873 import org.brainstorm.icon.*;
2874 global GlobalData
2875 % Get installed and supported plugins
2876 [PlugsInstalled, PlugsSupported]= GetInstalled();
2877 % Get previous menu entries
2878 jPlugs = GlobalData.Program.GUI.pluginMenus;
2879 % Regenerate plugin menu to look for new plugins
2880 jPlugs = MenuCreate(jMenu, jPlugs, PlugsSupported, fontSize);
2881 % Update menu entries
2882 GlobalData.Program.GUI.pluginMenus = jPlugs;
2883 % If compiled: disable most menus
2884 isCompiled = bst_iscompiled();
2885 % Interface scaling
2886 InterfaceScaling = bst_get('InterfaceScaling');
2887 % Update all the plugins
2888 for iPlug = 1:length(jPlugs)
2889 j = jPlugs(iPlug);
2890 PlugName = j.name;
2891 Plug = PlugsInstalled(ismember({PlugsInstalled.Name}, PlugName));
2892 PlugRef = PlugsSupported(ismember({PlugsSupported.Name}, PlugName));
2893 % Is installed?
2894 if ~isempty(Plug)
2895 isInstalled = 1;
2896 elseif ~isempty(PlugRef)
2897 Plug = PlugRef;
2898 isInstalled = 0;
2899 else
2900 disp(['BST> Error: Description not found for plugin: ' PlugName]);
2901 continue;
2902 end
2903 isLoaded = isInstalled && Plug.isLoaded;
2904 isManaged = isInstalled && Plug.isManaged;
2905 % Compiled included: no submenus
2906 if isCompiled && (PlugRef.CompiledStatus == 2)
2907 j.menu.setEnabled(1);
2908 if (InterfaceScaling ~= 100)
2909 j.menu.setIcon(IconLoader.scaleIcon(IconLoader.ICON_GOOD, InterfaceScaling / 100));
2910 else
2911 j.menu.setIcon(IconLoader.ICON_GOOD);
2912 end
2913 % Otherwise: all available
2914 else
2915 % Main menu: Available/Not available
2916 j.menu.setEnabled(isInstalled || ~isempty(Plug.URLzip));
2917 % Current version
2918 if ~isInstalled
2919 j.version.setText('<HTML><FONT color="#707070"><I>Not installed</I></FONT>');
2920 elseif ~isManaged && ~isempty(Plug.Path)
2921 j.version.setText('<HTML><FONT color="#707070"><I>Custom install</I></FONT>')
2922 elseif ~isempty(Plug.Version) && ischar(Plug.Version)
2923 strVer = Plug.Version;
2924 % If downloading from github
2925 if isGithubMaster(Plug.URLzip)
2926 % Show installation date, if available
2927 if ~isempty(Plug.InstallDate)
2928 strVer = Plug.InstallDate(1:11);
2929 % Show only the short SHA (7 chars)
2930 elseif (length(Plug.Version) >= 30)
2931 strVer = Plug.Version(1:7);
2932 end
2933 end
2934 j.version.setText(['<HTML><FONT color="#707070"><I>Installed version: ' strVer '</I></FONT>'])
2935 elseif isInstalled
2936 j.version.setText('<HTML><FONT color="#707070"><I>Installed</I></FONT>');
2937 end
2938 % Main menu: Icon
2939 if isCompiled && isInstalled
2940 menuIcon = IconLoader.ICON_GOOD;
2941 elseif isLoaded % Loaded
2942 menuIcon = IconLoader.ICON_GOOD;
2943 elseif isInstalled % Not loaded
2944 menuIcon = IconLoader.ICON_BAD;
2945 else
2946 menuIcon = IconLoader.ICON_NEUTRAL;
2947 end
2948 if (InterfaceScaling ~= 100)
2949 j.menu.setIcon(IconLoader.scaleIcon(menuIcon, InterfaceScaling / 100));
2950 else
2951 j.menu.setIcon(menuIcon);
2952 end
2953 % Install
2954 j.install.setEnabled(~isInstalled);
2955 if ~isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
2956 j.install.setText(['<HTML>Install <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
2957 else
2958 j.install.setText('Install');
2959 end
2960 % Update
2961 j.update.setEnabled(isManaged);
2962 if isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version)
2963 j.update.setText(['<HTML>Update <FONT color="#707070"><I>(' PlugRef.Version ')</I></FONT>'])
2964 else
2965 j.update.setText('Update');
2966 end
2967 % Uninstall
2968 j.uninstall.setEnabled(isManaged);
2969 % Custom install
2970 j.custom.setEnabled(~isManaged);
2971 if ~isempty(Plug.Path)
2972 j.custompath.setText(Plug.Path);
2973 else
2974 j.custompath.setText('Path not set');
2975 end
2976 % Load/Unload
2977 j.load.setEnabled(isInstalled && ~isLoaded && ~isCompiled);
2978 j.unload.setEnabled(isLoaded && ~isCompiled);
2979 % Web
2980 j.web.setEnabled(~isempty(Plug.URLinfo));
2981 % Extra menus: Update availability
2982 if ~isempty(Plug.ExtraMenus)
2983 for iMenu = 1:size(Plug.ExtraMenus,1)
2984 if (size(Plug.ExtraMenus,2) == 3) && ~isempty(Plug.ExtraMenus{3})
2985 if (strcmpi(Plug.ExtraMenus{3}, 'loaded') && isLoaded) ...
2986 || (strcmpi(Plug.ExtraMenus{3}, 'installed') && isInstalled) ...
2987 || (strcmpi(Plug.ExtraMenus{3}, 'always'))
2988 j.extra(iMenu).setEnabled(1);
2989 else
2990 j.extra(iMenu).setEnabled(0);
2991 end
2992 end
2993 end
2994 end
2995 end
2996 end
2997 j.menu.repaint()
2998 j.menu.getParent().repaint()
2999 end
3000
3001
3002 %% ===== SET CUSTOM PATH =====
3003 function SetCustomPath(PlugName, PlugPath)
3004 % Parse inputs
3005 if (nargin < 2) || isempty(PlugPath)
3006 PlugPath = [];
3007 end
3008 % Custom plugin paths
3009 PluginCustomPath = bst_get('PluginCustomPath');
3010 % Get plugin description
3011 PlugDesc = GetSupported(PlugName);
3012 if isempty(PlugDesc)
3013 return;
3014 end
3015 % Get installed plugin
3016 PlugInst = GetInstalled(PlugName);
3017 isInstalled = ~isempty(PlugInst);
3018 isManaged = isInstalled && PlugInst.isManaged;
3019 if isManaged
3020 bst_error(['Plugin ' PlugName ' is already installed by Brainstorm, uninstall it first.'], 0);
3021 return;
3022 end
3023 % Ask install path to user
3024 isWarning = 1;
3025 if isempty(PlugPath)
3026 PlugPath = uigetdir(PlugInst.Path, ['Select ' PlugName ' directory.']);
3027 if isequal(PlugPath, 0)
3028 PlugPath = [];
3029 end
3030 % If removal is requested
3031 elseif isequal(PlugPath, 0)
3032 PlugPath = [];
3033 isWarning = 0;
3034 end
3035 % If the directory did not change: nothing to do
3036 if (isInstalled && isequal(PlugInst.Path, PlugPath)) || (~isInstalled && isempty(PlugPath))
3037 return;
3038 end
3039 % Unload previous version
3040 if isInstalled && ~isempty(PlugInst.Path) && PlugInst.isLoaded
3041 Unload(PlugName);
3042 end
3043 % Check if this is a valid plugin folder
3044 if isempty(PlugPath) || ~file_exist(PlugPath)
3045 PlugPath = [];
3046 end
3047 if ~isempty(PlugPath) && ~isempty(PlugDesc.TestFile)
3048 isValid = 0;
3049 if file_exist(bst_fullfile(PlugPath, PlugDesc.TestFile))
3050 isValid = 1;
3051 elseif ~isempty(PlugDesc.LoadFolders)
3052 for iFolder = 1:length(PlugDesc.LoadFolders)
3053 if file_exist(bst_fullfile(PlugPath, PlugDesc.LoadFolders{iFolder}, PlugDesc.TestFile))
3054 isValid = 1;
3055 end
3056 end
3057 end
3058 if ~isValid
3059 PlugPath = [];
3060 end
3061 end
3062 % Save path
3063 PluginCustomPath.(PlugName) = PlugPath;
3064 bst_set('PluginCustomPath', PluginCustomPath);
3065 % Load plugin
3066 if ~isempty(PlugPath)
3067 [isOk, errMsg, PlugDesc] = Load(PlugName);
3068 % Ignored warnings
3069 elseif ~isWarning
3070 isOk = 1;
3071 errMsg = [];
3072 % Invalid path
3073 else
3074 isOk = 0;
3075 if ~isempty(PlugDesc.TestFile)
3076 errMsg = ['The file ' PlugDesc.TestFile ' could not be found in selected folder.'];
3077 else
3078 errMsg = 'No valid folder was found.';
3079 end
3080 end
3081 % Handle errors
3082 if ~isOk
3083 bst_error(['An error occurred while configuring plugin ' PlugName ':' 10 10 errMsg 10], 'Plugin manager', 0);
3084 elseif ~isempty(errMsg)
3085 java_dialog('msgbox', ['Configuration message:' 10 10 errMsg 10], 'Plugin manager');
3086 elseif isWarning
3087 java_dialog('msgbox', ['Plugin ' PlugName ' successfully loaded.']);
3088 end
3089 end
3090
3091
3092 %% ===== ARCHIVE SOFTWARE ENVIRONMENT =====
3093 % USAGE: Archive(OutputFile=[ask])
3094 function Archive(OutputFile)
3095 % Parse inputs
3096 if (nargin < 1) || isempty(OutputFile)
3097 OutputFile = [];
3098 end
3099 % Get date string
3100 c = clock();
3101 strDate = sprintf('%02d%02d%02d', c(1)-2000, c(2), c(3));
3102 % Get output filename
3103 if isempty(OutputFile)
3104 % Get default directories
3105 LastUsedDirs = bst_get('LastUsedDirs');
3106 % Default output filename
3107 OutputFile = bst_fullfile(LastUsedDirs.ExportScript, ['bst_env_' strDate '.zip']);
3108 % File selection
3109 OutputFile = java_getfile('save', 'Export environment', OutputFile, 'single', 'files', ...
3110 {{'.zip'}, 'Zip files (*.zip)', 'ZIP'}, 1);
3111 if isempty(OutputFile)
3112 return
3113 end
3114 % Save new default export path
3115 LastUsedDirs.ExportScript = bst_fileparts(OutputFile);
3116 bst_set('LastUsedDirs', LastUsedDirs);
3117 end
3118
3119 % ===== TEMP FOLDER =====
3120 bst_progress('start', 'Export environment', 'Creating temporary folder...');
3121
3122 % ===== COPY BRAINSTORM =====
3123 bst_progress('text', 'Copying: brainstorm...');
3124 % Get Brainstorm path and version
3125 bstVer = bst_get('Version');
3126 bstDir = bst_get('BrainstormHomeDir');
3127 % Create temporary folder for storing all the files to package
3128 TmpDir = bst_get('BrainstormTmpDir', 0, 'bstenv');
3129 % Get brainstorm3 destination folder: add version number
3130 if ~isempty(bstVer.Version) && ~any(bstVer.Version == '?')
3131 envBst = bst_fullfile(TmpDir, ['brainstorm', bstVer.Version]);
3132 else
3133 [tmp, bstName] = bst_fileparts(bstDir);
3134 envBst = bst_fullfile(TmpDir, bstName);
3135 end
3136 % Add git commit hash
3137 if (length(bstVer.Commit) >= 30)
3138 envBst = [envBst, '_', bstVer.Commit(1:7)];
3139 end
3140 % Copy brainstorm3 folder
3141 isOk = file_copy(bstDir, envBst);
3142 if ~isOk
3143 error(['Cannot copy folder: "' bstDir '" to "' envBst '"']);
3144 end
3145
3146 % ===== COPY DEFAULTS =====
3147 bst_progress('text', 'Copying: user defaults...');
3148 % Get user defaults folder
3149 userDef = bst_get('UserDefaultsDir');
3150 envDef = bst_fullfile(envBst, 'defaults');
3151 isOk = file_copy(userDef, envDef);
3152 if ~isOk
3153 error(['Cannot merge folder: "' userDef '" into "' envDef '"']);
3154 end
3155
3156 % ===== COPY USER PROCESSES =====
3157 bst_progress('text', 'Copying: user processes...');
3158 % Get user process folder
3159 userProc = bst_get('UserProcessDir');
3160 envProc = bst_fullfile(envBst, 'toolbox', 'process', 'functions');
3161 isOk = file_copy(userProc, envProc);
3162 if ~isOk
3163 error(['Cannot merge folder: "' userProc '" into "' envProc '"']);
3164 end
3165
3166 % ===== COPY PLUGINS ======
3167 % Get list of plugins to package
3168 PlugDesc = GetInstalled();
3169 % Destination plugin directory
3170 envPlugins = bst_fullfile(envBst, 'plugins');
3171 % Copy each installed plugin
3172 for iPlug = 1:length(PlugDesc)
3173 bst_progress('text', ['Copying plugin: ' PlugDesc(iPlug).Name '...']);
3174 envPlug = bst_fullfile(envPlugins, PlugDesc(iPlug).Name);
3175 isOk = file_copy(PlugDesc(iPlug).Path, envPlug);
3176 if ~isOk
3177 error(['Cannot copy folder: "' PlugDesc(iPlug).Path '" into "' envProc '"']);
3178 end
3179 end
3180 % Copy user-defined JSON files
3181 PlugJson = dir(fullfile(bst_get('UserPluginsDir'), 'plugin_*.json'));
3182 for iPlugJson = 1:length(PlugJson)
3183 bst_progress('text', ['Copying use-defined plugin JSON file: ' PlugJson(iPlugJson).name '...']);
3184 plugJsonFile = bst_fullfile(PlugJson(iPlugJson).folder, PlugJson(iPlugJson).name);
3185 envPlugJson = bst_fullfile(envPlugins, PlugJson(iPlugJson).name);
3186 isOk = file_copy(plugJsonFile, envPlugJson);
3187 if ~isOk
3188 error(['Cannot copy file: "' plugJsonFile '" into "' envProc '"']);
3189 end
3190 end
3191
3192 % ===== SAVE LIST OF VERSIONS =====
3193 strList = bst_plugin('List', 'installed', 0);
3194 % Open file versions.txt
3195 VersionFile = bst_fullfile(TmpDir, 'versions.txt');
3196 fid = fopen(VersionFile, 'wt');
3197 if (fid < 0)
3198 error(['Cannot save file: ' VersionFile]);
3199 end
3200 % Save Brainstorm plugins list
3201 fwrite(fid, strList);
3202 % Save Matlab ver command
3203 strMatlab = evalc('ver');
3204 fwrite(fid, [10 10 strMatlab]);
3205 % Close file
3206 fclose(fid);
3207
3208 % ===== ZIP FILES =====
3209 bst_progress('text', 'Zipping environment...');
3210 % Zip files with bst_env_* being the first level
3211 zip(OutputFile, TmpDir, bst_fileparts(TmpDir));
3212 % Delete the temporary files
3213 file_delete(TmpDir, 1, 1);
3214 % Close progress bar
3215 bst_progress('stop');
3216 end
3217
3218
3219 %% ============================================================================
3220 % ===== PLUGIN-SPECIFIC FUNCTIONS ============================================
3221 % ============================================================================
3222
3223 %% ===== LINK TOOLBOX-SPM =====
3224 % USAGE: bst_plugin('LinkSpmToolbox', Action)
3225 % 0=Delete/1=Create/2=Check a symbolic link for a Toolbox in SPM12 toolbox folder
3226 function LinkSpmToolbox(Action, ToolboxName)
3227 % Get SPM12 plugin
3228 PlugSpm = GetInstalled('spm12');
3229 if isempty(PlugSpm)
3230 error('Plugin SPM12 is not loaded.');
3231 elseif ~PlugSpm.isLoaded
3232 [isOk, errMsg, PlugSpm] = Load('spm12');
3233 if ~isOk
3234 error('Plugin SPM12 cannot be loaded.');
3235 end
3236 end
3237 % Get SPM plugin path
3238 if ~isempty(PlugSpm.SubFolder)
3239 spmToolboxDir = bst_fullfile(PlugSpm.Path, PlugSpm.SubFolder, 'toolbox');
3240 else
3241 spmToolboxDir = bst_fullfile(PlugSpm.Path, 'toolbox');
3242 end
3243 if ~file_exist(spmToolboxDir)
3244 error(['Could not find SPM12 toolbox folder: ' spmToolboxDir]);
3245 end
3246 % Toolbox plugin path
3247 spmToolboxDirTarget = bst_fullfile(spmToolboxDir, ToolboxName);
3248 % Get toolbox plugin
3249 PlugToolbox = GetInstalled(ToolboxName);
3250
3251 % Check link
3252 if (Action == 2)
3253 % Link exists and works: return here
3254 if file_exist(bst_fullfile(spmToolboxDirTarget, PlugToolbox.TestFile))
3255 return;
3256 % Link doesn't exist: Create it
3257 else
3258 Action = 1;
3259 end
3260 end
3261 % If folder already exists
3262 if file_exist(spmToolboxDirTarget)
3263 % If setting install and SPM is not managed by Brainstorm: do not risk deleting user's install
3264 if (Action == 1) && ~PlugSpm.isManaged
3265 error([upper(ToolboxName) ' seems already set up: ' spmToolboxDirTarget]);
3266 end
3267 % All the other cases: delete existing toolbox folder
3268 if ispc
3269 rmCall = ['rmdir /q /s "' spmToolboxDirTarget '"'];
3270 else
3271 rmCall = ['rm -rf "' spmToolboxDirTarget '"'];
3272 end
3273 disp(['BST> Deleting existing SPM12 toolbox: ' rmCall]);
3274 [status,result] = system(rmCall);
3275 if (status ~= 0)
3276 error(['Error deleting link: ' result]);
3277 end
3278 end
3279 % Create new link
3280 if (Action == 1)
3281 if isempty(PlugToolbox) || ~PlugToolbox.isLoaded
3282 error(['Plugin ' upper(ToolboxName) ' is not loaded.']);
3283 end
3284 % Return if installation is not complete yet (first load before installation ends)
3285 if isempty(PlugToolbox.InstallDate)
3286 return
3287 end
3288 % Define source and target for the link
3289 if ~isempty(PlugToolbox.SubFolder)
3290 linkTarget = bst_fullfile(PlugToolbox.Path, PlugToolbox.SubFolder);
3291 else
3292 linkTarget = PlugToolbox.Path;
3293 end
3294 linkFile = spmToolboxDirTarget;
3295 % Create link
3296 if ispc
3297 linkCall = ['mklink /D "' linkFile '" "' linkTarget '"'];
3298 else
3299 linkCall = ['ln -s "' linkTarget '" "' linkFile '"'];
3300 end
3301 disp(['BST> Creating symbolic link: ' linkCall]);
3302 [status,result] = system(linkCall);
3303 if (status ~= 0)
3304 error(['Error creating link: ' result]);
3305 end
3306 end
3307 end
3308
3309
3310 %% ===== SET PROGRESS LOGO =====
3311 % USAGE: SetProgressLogo(PlugDesc/PlugName) % Set progress bar image
3312 % SetProgressLogo([]) % Remove progress bar image
3313 function SetProgressLogo(PlugDesc)
3314 % Remove image
3315 if (nargin < 1) || isempty(PlugDesc)
3316 bst_progress('removeimage');
3317 bst_progress('removelink');
3318 % Set image
3319 else
3320 % Get plugin description
3321 if ischar(PlugDesc)
3322 PlugDesc = GetSupported(PlugDesc);
3323 end
3324 % Set logo file
3325 if isempty(PlugDesc.LogoFile)
3326 PlugDesc.LogoFile = GetLogoFile(PlugDesc);
3327 end
3328 if ~isempty(PlugDesc.LogoFile)
3329 bst_progress('setimage', PlugDesc.LogoFile);
3330 end
3331 % Set link
3332 if ~isempty(PlugDesc.URLinfo)
3333 bst_progress('setlink', PlugDesc.URLinfo);
3334 end
3335 end
3336 end
3337
3338
3339 %% ===== NOT SUPPORTED APPLE SILICON =====
3340 % Return list of plugins not supported on Apple silicon
3341 function pluginNames = PluginsNotSupportAppleSilicon()
3342 pluginNames = { 'duneuro', 'mcxlab-cuda'};
3343 end
3344
3345 %% ===== MATCH STRING EDGES =====
3346 % Check if a string 'strA' starts (or ends) with string B
3347 function result = strMatchEdge(a, b, edge)
3348 b = regexptranslate('escape', b);
3349 if strcmpi(edge, 'start')
3350 result = ~isempty(regexp(a, ['^', b], 'once'));
3351 elseif strcmpi(edge, 'end')
3352 result = ~isempty(regexp(a, [b, '$'], 'once'));
3353 else
3354 result = 0;
3355 end
3356 end