home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Chip 2007 January, February, March & April
/
Chip-Cover-CD-2007-02.iso
/
boot
/
i386
/
root
/
usr
/
share
/
YaST2
/
modules
/
AddOnProduct.ycp
< prev
next >
Wrap
Text File
|
2006-11-29
|
26KB
|
945 lines
/**
* File:
* AddOnProduct.ycp
*
* Module:
* AddOnProduct
*
* Summary:
* This module provides integration of the add-on products
*
* Authors:
* Jiri Srain<jsrain@suse.de>
*/
{
module "AddOnProduct";
textdomain "installation";
import "Label";
import "Mode";
import "ProductControl";
import "ProductFeatures";
import "Report";
import "XML";
import "Wizard";
import "FileUtils";
import "Language";
import "Popup";
import "InstShowInfo";
import "ProductLicense";
// variables for installation with product
/**
* ID for cache in the inst-sys
*/
integer src_cache_id = -1;
/**
* System proposals have already been prepared for merging?
*/
boolean system_proposals_prepared = false;
/**
* System workflows have already been prepared for merging?
*/
boolean system_workflows_prepared = false;
/**
* List of all selected sources
*/
global list<map<string,any> > add_on_products = [];
/**
* ID of currently added source for the add-on product
*/
global integer src_id = nil;
// for the add-on product workflow - needed for dialog skipping
/**
* return value of last step in the product adding workflow
*/
global symbol last_ret = nil;
/**
* List of used control files, to be copied to target system
*/
global list<string> control_files_to_add = [];
/**
* Items if add-on product to be performed at the start of inst_finish
*/
global list<string> finish_steps_before_chroot = [];
/**
* Items of add-on product to be performed after swictching to chroot
*/
global list<string> finish_steps_after_chroot = [];
/**
* Items of add-on product to be performed before unmounting disks
*/
global list<string> finish_steps_before_umount = [];
global boolean modified = false;
global list<integer> mode_config_sources = [];
/**
* Adapts the inst-sys from the tarball
* @param filename string the filename with the tarball to use to the update
* @return boolean true on success
*/
global boolean UpdateInstSys (string filename) {
src_cache_id = src_cache_id + 1;
string tmpdir = (string)SCR::Read (.target.tmpdir);
tmpdir = sformat ("%1/%2", tmpdir, src_cache_id);
map out = (map)SCR::Execute (.target.bash_output, sformat ("
/bin/mkdir %1;
cd %1;
/bin/tar -xvf %2;
/sbin/adddir %1 /;
", tmpdir, filename));
if (out["exit"]:0 != 0)
{
y2error ("Including installation image failed: %1", out);
return false;
}
y2milestone ("Including installation image succeeded");
return true;
}
/**
* Check all proposals, split those ones which have multiple modes or
* architectures or stages into multiple proposals
* @param proposals a list of proposals
* @return a list of updated proposals
*/
list<map> PrepareProposals(list<map> proposals) {
list<map> new_proposals = [];
foreach (map p, proposals, {
string mode = p["mode"]:"";
list<string> modes = splitstring (mode, ",");
if (size(modes) == 0)
modes = [""];
foreach (string m, modes, {
map mp = p;
mp["mode"] = m;
string arch = p["archs"]:"";
list<string> archs = splitstring (arch, ",");
if (size(archs) == 0)
archs = [""];
foreach (string a, archs, {
map amp = mp;
amp["archs"] = a;
string stage = amp["stage"]:"";
list<string> stages = splitstring(stage, ",");
if (size (stages) == 0)
stages = [""];
foreach (string s, stages, {
map samp = amp;
samp["stage"] = s;
new_proposals = add (new_proposals, samp);
});
});
});
});
return new_proposals;
}
/**
* Check all proposals, split those ones which have multiple modes or
* architectures or stages into multiple proposals.
* Works with base product proposals.
*/
void PrepareSystemProposals() {
if (system_proposals_prepared)
return;
ProductControl::proposals = PrepareProposals(ProductControl::proposals);
system_proposals_prepared = true;
}
/**
* Check all workflows, split those ones which have multiple modes or
* architectures or stages into multiple workflows
* @param workflows a list of workflows
* @return a list of updated workflows
*/
list<map> PrepareWorkflows(list<map> workflows) {
list<map> new_workflows = [];
foreach (map w, workflows, {
string mode = w["mode"]:"";
list<string> modes = splitstring (mode, ",");
if (size(modes) == 0)
modes = [""];
foreach (string m, modes, {
map mw = w;
mw["mode"] = m;
mw["defaults"] = mw["defaults"]:$[];
string arch = mw["defaults", "archs"]:"";
list<string> archs = splitstring (arch, ",");
if (size(archs) == 0)
archs = [""];
foreach (string a, archs, {
map amw = mw;
amw["defaults", "archs"] = a;
string stage = amw["stage"]:"";
list<string> stages = splitstring(stage, ",");
if (size (stages) == 0)
stages = [""];
foreach (string s, stages, {
map samw = amw;
samw["stage"] = s;
new_workflows = add (new_workflows, samw);
});
});
});
});
return new_workflows;
}
/**
* Check all workflows, split those ones which have multiple modes or
* architectures or stages into multiple worlflows.
* Works with base product workflows.
*/
void PrepareSystemWorkflows() {
if (system_workflows_prepared)
return;
ProductControl::workflows = PrepareWorkflows(ProductControl::workflows);
system_workflows_prepared = true;
}
/**
* Replace a module in a proposal with a set of other modules
* @param proposal a map describing the proposal
* @param old string the old item to be replaced
* @param new a list of items to be put into instead of the old one
* @return a map with the updated proposal
*/
map ReplaceProposalModule(map proposal, string old, list<string> new) {
boolean found = false;
list<list> modules = maplist (any m, proposal["proposal_modules"]:[], {
if ((is (m, string) && (string)m == old)
|| is (m, map) && ((map)m)["name"]:"" == old)
{
found = true;
if (is (m, map))
{
return maplist (string it, new, {
return union ((map)m, $[ "name" : it ]);
});
}
else
{
return new;
}
}
else
{
return [m];
}
});
if (! found)
y2internal ("Replace/Remove proposal item %1 not found", old);
proposal["proposal_modules"] = flatten(modules);
if (haskey (proposal, "proposal_tabs"))
{
proposal["proposal_tabs"] = maplist (map tab,
proposal["proposal_tabs"]:[],
{
list<list<string> > modules = maplist (string m,
tab["proposal_modules"]:[],
{
if (m == old)
return new;
else
return [ m ];
});
tab["proposal_modules"] = flatten(modules);
return tab;
});
}
return proposal;
}
/**
* Merge add-on proposal to a base proposal
* @param base map the base product proposal
* @param addon map the proposal of the addon productA
* @param prod_name a name of the add-on product
* @return map merged proposals
*/
map MergeProposal(map base, map addon, string prod_name, string domain)
{
list<string> appends = addon["append_modules"]:[];
list<string> removes = addon["remove_modules"]:[];
map<string,list<string> > replaces = listmap(map a,
addon["replace_modules"]:[],
{
string old = a["replace"]:"";
list<string> new = a["modules"]:[];
return $[ old : new ];
});
if (size (removes) > 0)
{
foreach (string r, removes, {
base = ReplaceProposalModule (base, r, []);
});
}
if (size (replaces) > 0)
{
foreach (string old, list<string> new, replaces, {
base = ReplaceProposalModule (base, old, new);
});
}
if (size (appends) > 0)
{
boolean as_map = false;
list<any> append2 = appends;
if (is (base["proposal_modules", 0]:nil, map))
{
append2 = maplist (string m, appends, {
return $[ "name" : m, "presentation_order" : 9999 ];
});
}
base["proposal_modules"] = merge (base["proposal_modules"]:[], append2);
if (haskey (base, "proposal_tabs"))
{
map new_tab = $[
"label" : prod_name,
"proposal_modules" : appends,
"textdomain" : domain,
];
base["proposal_tabs"] = add (base["proposal_tabs"]:[], new_tab);
}
}
if (addon["enable_skip"]:"yes" == "no")
base["enable_skip"] = "no";
return base;
}
/**
* Update system proposals according to proposal update metadata
* @param proposals a list of update proposals
* @param prod_name string the product name (used in case of tabs)
* @param domain string the text domain (for translations)
* @return boolean true on success
*/
boolean UpdateProposals(list<map> proposals, string prod_name, string domain) {
foreach (map proposal, proposals, {
string name = proposal["name"]:"";
string stage = proposal["stage"]:"";
string mode = proposal["mode"]:"";
string arch = proposal["archs"]:"";
boolean found = false;
list<map> new_proposals = [];
map arch_all_prop = $[];
foreach (map p, ProductControl::proposals, {
if (p["stage"]:"" != stage || p["mode"]:"" != mode
|| p["name"]:"" != name)
{
new_proposals = add (new_proposals, p);
continue;
}
if (p["archs"]:"" == arch || arch == "" || arch == "all")
{
p = MergeProposal (p, proposal, prod_name, domain);
found = true;
}
else if (p["archs"]:"" == "" || p["archs"]:"" == "all")
{
arch_all_prop = p;
}
new_proposals = add (new_proposals, p);
});
if (! found)
{
if (arch_all_prop != $[])
{
arch_all_prop["archs"] = arch;
proposal = MergeProposal (arch_all_prop, proposal,
prod_name, domain);
}
else // completly new proposal
{
proposal["textdomain"] = domain;
}
new_proposals = add (new_proposals, proposal);
}
ProductControl::proposals = new_proposals;
});
return true;
}
/**
* Replace a module in a workflow with a set of other modules
* @param workflow a map describing the workflow
* @param old string the old item to be replaced
* @param new a list of items to be put into instead of the old one
* @param domain string a text domain
* @param keep boolean true to keep original one (and just insert before)
* @return a map with the updated workflow
*/
map ReplaceWorkflowModule(map workflow, string old, list<map> new,
string domain, boolean keep)
{
boolean found = false;
list<list<map> > modules = maplist (map m, workflow["modules"]:[], {
if (m["name"]:"" == old)
{
list<map> new_list = maplist (map n, new, {
n["textdomain"] = domain;
return n;
});
found = true;
if (keep)
new_list = add (new_list, m);
return new_list;
}
else
{
return [m];
}
});
if (! found)
y2internal ("Insert/Replace/Remove workflow module %1 not found", old);
workflow["modules"] = flatten(modules);
return workflow;
}
/**
* Merge add-on workflow to a base workflow
* @param base map the base product workflow
* @param addon map the workflow of the addon product
* @param prod_name a name of the add-on product
* @return map merged workflows
*/
map MergeWorkflow(map base, map addon, string prod_name, string domain)
{
list<map> appends = addon["append_modules"]:[];
list<string> removes = addon["remove_modules"]:[];
map<string,list<map> > inserts = listmap (map i,
addon["insert_modules"]:[],
{
string before = i["before"]:"";
list<map> new = i["modules"]:[];
return $[ before : new ];
});
map<string,list<map> > replaces = listmap(map a,
addon["replace_modules"]:[],
{
string old = a["replace"]:"";
list<map> new = a["modules"]:[];
return $[ old : new ];
});
if (size (removes) > 0)
{
foreach (string r, removes, {
base = ReplaceWorkflowModule (base, r, [], domain, false);
});
}
if (size (replaces) > 0)
{
foreach (string old, list<map> new, replaces, {
base = ReplaceWorkflowModule (base, old, new, domain, false);
});
}
if (size (inserts) > 0)
{
foreach (string old, list<map> new, inserts, {
base = ReplaceWorkflowModule (base, old, new, domain, true);
});
}
if (size (appends) > 0)
{
foreach (map new, appends, {
new["textdomain"] = domain;
base["modules"] = add (base["modules"]:[], new);
});
}
return base;
}
/**
* Update system workflows according to workflow update metadata
* @param workflows a list of update workflows
* @param prod_name string the product name (used in case of tabs)
* @param domain string the text domain (for translations)
* @return boolean true on success
*/
boolean UpdateWorkflows(list<map> workflows, string prod_name, string domain) {
foreach (map workflow, workflows, {
string stage = workflow["stage"]:"";
string mode = workflow["mode"]:"";
string arch = workflow["archs"]:"";
boolean found = false;
list<map> new_workflows = [];
map arch_all_wf = $[];
foreach (map w, ProductControl::workflows, {
if (w["stage"]:"" != stage || w["mode"]:"" != mode)
{
new_workflows = add (new_workflows, w);
continue;
}
if (w["defaults", "archs"]:"" == arch
|| arch == "" || arch == "all")
{
w = MergeWorkflow (w, workflow, prod_name, domain);
found = true;
}
else if (w["defaults", "archs"]:"" == ""
|| w["default", "archs"]:"" == "all")
{
arch_all_wf = w;
}
new_workflows = add (new_workflows, w);
});
if (! found)
{
if (arch_all_wf != $[])
{
arch_all_wf["defaults", "archs"] = arch;
workflow = MergeWorkflow (arch_all_wf, workflow,
prod_name, domain);
}
else // completly new workflow
{
workflow["textdomain"] = domain;
workflow["modules"] = maplist (map mod, workflow["modules"]:[], {
mod["textdomain"] = domain;
return mod;
});
}
new_workflows = add (new_workflows, workflow);
}
ProductControl::workflows = new_workflows;
});
return true;
}
/**
* Redraw the wizard steps bar
* @return boolean true on success
*/
boolean RedrawWizardSteps () {
// FIXME following code copy-pasted from clients/installation.ycp
y2milestone( "Retranslating messages" );
// Make sure the labels for default function keys are retranslated, too.
// Using Label::DefaultFunctionKeyMap() from Label module.
UI::SetFunctionKeys( Label::DefaultFunctionKeyMap() );
// Activate language changes on static part of wizard dialog
ProductControl::RetranslateWizardSteps();
Wizard::RetranslateButtons();
Wizard::SetFocusToNextButton();
// FIXME end of copy-paste
return true;
}
/**
* Add specified steps to inst_finish
* Just modifies internal variables, inst_finish grabs them itself
* @param additional_steps a map specifying the steps to be added
* @return boolean true on success
*/
boolean UpdateInstFinish (map<string,list<string> > additional_steps) {
list<string> before_chroot = additional_steps["before_chroot"]:[];
list<string> after_chroot = additional_steps["after_chroot"]:[];
list<string> before_umount = additional_steps["before_umount"]:[];
finish_steps_before_chroot = (list<string>)
merge (finish_steps_before_chroot, before_chroot);
finish_steps_after_chroot = (list<string>)
merge (finish_steps_after_chroot, after_chroot);
finish_steps_before_umount = (list<string>)
merge (finish_steps_before_umount, before_umount);
return true;
}
/**
* Adapts the installation workflow according to specified XML file
* @param update_file a map containing the add-on product control file
* @param name string the name of the add-on product
* @param domain string the text domain for the add-on product
* @return boolean true on success
*/
boolean UpdateInstallation (map update_file, string name, string domain) {
PrepareSystemProposals ();
PrepareSystemWorkflows ();
list<map> proposals = update_file["proposals"]:[];
proposals = PrepareProposals (proposals);
UpdateProposals (proposals, name, domain);
list<map> workflows = update_file["workflows"]:[];
workflows = PrepareWorkflows (workflows);
UpdateWorkflows (workflows, name, domain);
return true;
}
/**
* Add new defined proposal to the list of system proposals
* @param proposals a lsit of proposals to be added
* @return boolean true on success
*/
boolean AddNewProposals (list<map> proposals) {
list<string> forbidden = maplist (map p, ProductControl::proposals, {
return p["name"]:"";
});
forbidden = toset (forbidden);
foreach (map proposal, proposals, {
if (! contains (forbidden, proposal["name"]:""))
{
y2milestone ("Adding new proposal %1", proposal["name"]:"");
ProductControl::proposals = add (ProductControl::proposals, proposal);
}
});
return true;
}
/**
* Replace workflows for 2nd stage of installation
* @param workflows a list of the workflows
* @return boolean true on success
*/
boolean Replaceworkflows (list<map> workflows) {
workflows = PrepareWorkflows (workflows);
workflows = filter (map w, workflows, {
if (w["stage"]:"" == "initial")
{
y2error ("Attempting to replace 1st stage workflow. This is not possible");
return false;
}
return true;
});
map<string,map<string, boolean> > sm = $[];
foreach (map w, workflows, {
sm[w["stage"]:""] = sm[w["stage"]:""]:$[];
sm[w["stage"]:"", w["mode"]:""] = true;
return [w["stage"]:"", w["mode"]:""];
});
y2milestone ("Existing replace workflows: %1", sm);
y2milestone ("Workflows before filtering: %1", size (ProductControl::workflows));
ProductControl::workflows = filter (map w, ProductControl::workflows, {
return ! sm[w["stage"]:"", w["mode"]:""]:false;
});
y2milestone ("Workflows after filtering: %1", size (ProductControl::workflows));
ProductControl::workflows = (list<map>)merge (ProductControl::workflows, workflows);
return true;
}
/**
* Update product options
* @param update_file a map containing update control file
* @return boolean true on success
*/
boolean UpdateProductInfo(map update_file) {
foreach (string section, ["globals", "software", "partitioning", "network"],
{
map<string,any> sect = ProductFeatures::GetSection(section);
map<string,any> addon = update_file[section]:$[];
sect = (map<string,any>)union (sect, addon);
ProductFeatures::SetSection(section, sect);
});
list<string> addon_clone = update_file["clone_modules"]:[];
ProductControl::clone_modules
= (list<string>)merge (ProductControl::clone_modules, addon_clone);
return true;
}
/**
* Remove the /y2update directory from the system
*/
void CleanY2Update() {
SCR::Execute (.target.bash, "/bin/rm -rf /y2update");
}
/**
* Show /media.1/info.txt file in a pop-up message if such file exists.
* Show license if such exists and return whether users accepts it.
* Returns 'nil' when did not succed.
*
* @return boolean whether the license has been accepted
*/
global boolean AcceptedLicenseAndInfoFile (integer src_id) {
symbol ret = ProductLicense::AskAddOnLicenseAgreement (src_id);
if (ret == nil)
return nil;
else if (ret == `abort || ret == `back)
{
y2milestone ("License confirmation failed");
return false;
}
return true;
}
/**
* Do installation of the add-on product within an installed system
* srcid is got via AddOnProduct::src_id
*
* @param string src_id
* @return symbol the result symbol from wizard sequencer
*/
global symbol DoInstall() {
// Display /media.1/info.txt if such file exists
// Display license and wait for agreement
// Not needed here, license already shown in the workflow
/*
boolean license_ret = AcceptedLicenseAndInfoFile(src_id);
if (license_ret != true) {
y2milestone("Removing the current source ID %1", src_id);
Pkg::SourceDelete(src_id);
return nil;
}
*/
string control = Pkg::SourceProvideOptionalFile (src_id, 1, "/installation.xml");
// Fallback -- Source didn't provide needed controll file
// Handling as it was an installation source
if (control == nil)
{
y2milestone("File /installation.xml not found, running sw_single for this source");
WFM::CallFunction ("sw_single", []);
return nil;
}
// copy the control file to local filesystem - in case of media release
string tmp = (string)SCR::Read (.target.tmpdir);
tmp = tmp + "/installation.xml";
SCR::Execute (.target.bash, sformat ("/bin/cp %1 %2", control, tmp));
control = tmp;
string binaries = Pkg::SourceProvideOptionalFile (src_id, 1, "/y2update.tgz");
// File /y2update.tgz exists
if (binaries != nil)
{
// Try to extract files from the archive
map out = (map)SCR::Execute (.target.bash_output, sformat ("
test -d /y2update && rm -rf /y2update;
/bin/mkdir -p /y2update/all;
cd /y2update/all;
/bin/tar -xvf %1;
cd /y2update;
ln -s all/usr/share/YaST2/* .;
ln -s all/usr/lib/YaST2/* .;
", binaries));
// Failed
if (out["exit"]:0 != 0)
{
// error report
Report::Error (_("An error occurred while preparing the installation system."));
CleanY2Update();
return nil;
}
}
else
{
y2milestone("File /y2update.tgz not provided");
}
// set control file
ProductControl::custom_control_file = control;
if (!ProductControl::Init())
{
// error report
Report::Error (sformat (_("Control file %1 not found on media."),
control));
CleanY2Update();
return nil;
}
// start workflow
Wizard::OpenNextBackStepsDialog();
// dialog caption
Wizard::SetContents(_("Initializing..."), `Empty (), "", false, false);
list<map> stage_mode = [$["stage": "normal", "mode": "installation" ]];
ProductControl::AddWizardSteps(stage_mode);
Mode::SetMode ("installation");
symbol ret = ProductControl::Run();
UI::CloseDialog();
CleanY2Update();
return ret;
}
/**
* Integrate the changes in the workflow
* @param filename string filename of the control file (local filename)
* @return boolean true on success
*/
global boolean WFIntegrate (string filename) {
map update_file = XML::XMLToYCPFile (filename);
string name = update_file["display_name"]:"";
boolean ret = UpdateInstallation (update_file["update"]:$[], name,
update_file["textdomain"]:"control");
if (! ret)
{
y2error ("Failed to udpate installation workflow");
return false;
}
if (! UpdateProductInfo (update_file))
{
y2error ("Failed to set product options");
return false;
}
if (! AddNewProposals (update_file["proposals"]:[]))
{
y2error ("Failed to add new proposals");
return false;
}
if (! Replaceworkflows (update_file["workflows"]:[]))
{
y2error ("Failed to replace workflows");
return false;
}
if (! RedrawWizardSteps ())
{
y2error ("Redrawing the wizard steps failed");
return false;
}
if (! UpdateInstFinish (update_file["update", "inst_finish"]:$[]))
{
y2error ("Adding inst_finish steps failed");
return false;
}
return true;
}
/**
* Integrate the add-on product to the installation workflow, including
* preparations for 2nd stage and inst-sys update
* @param srcid integer the source ID of the installation source
* @return boolean true on success
*/
global boolean Integrate (integer srcid) {
y2milestone ("Integrating source %1", srcid);
string filename = Pkg::SourceProvideOptionalFile (srcid, 1, "/installation.xml");
if (filename == nil)
{
y2milestone ("Add-on product control file not found, not touching the workglow");
}
else
{
if (! WFIntegrate (filename))
{
y2error ("Workflow update failed");
return false;
}
string tmpdir = (string)SCR::Read (.target.tmpdir) + "/control_files";
map out = (map)SCR::Execute (.target.bash_output, sformat ("
test -d %1 || /bin/mkdir %1;
/bin/cp %2 %1/%3.xml;
", tmpdir, filename, srcid));
if (out["exit"]:-1 != 0)
{
y2error ("Error occurred while copying control file: %1", out);
}
control_files_to_add = add (control_files_to_add, sformat ("%1.xml", srcid));
}
string y2update = Pkg::SourceProvideOptionalFile (srcid, 1, "/y2update.tgz");
if (y2update == nil)
{
y2milestone ("No YaST update found on the media");
}
else
{
UpdateInstSys (y2update);
}
return true;
}
global boolean CheckProductDependencies (list<string> products) {
// TODO check the dependencies of the product
return true;
}
/**
* Auto-integrate add-on products in specified file
* @param filelist string a file containing a list of add-on products to integrate
* @return boolean true on exit
*/
global boolean AddPreselectedAddOnProducts(string filelist) {
if (filelist == nil)
{
y2milestone ("No add-on products defined on the media");
return true;
}
list<string> products = splitstring ((string)SCR::Read (.target.string, filelist), "\r\n");
foreach (string p, products, {
if (p == "")
return;
list<string> elements = splitstring (p, " \t");
elements = filter (string e, elements, { return e != ""; });
string url = elements[0]:"";
string pth = elements[1]:"/";
elements = remove (elements, 0);
elements = remove (elements, 0);
integer src = Pkg::SourceCreate (url, pth);
if (! AcceptedLicenseAndInfoFile(src))
{
Pkg::SourceDelete (src);
return;
}
Integrate (src);
if (size (elements) > 0)
{
foreach (string e, elements, {
Pkg::ResolvableInstall (e, `product);
});
}
else
{
list<map<string,any> > products = Pkg::ResolvableProperties ("", `product, "");
products = filter (map<string,any> p, products, {
return p["source"]:-1 == src;
});
foreach (map<string,any> p, products, {
Pkg::ResolvableInstall (p["name"]:"", `product);
});
}
});
}
global map Export () {
list<map<string,any> > exp = maplist (map<string,any> p, add_on_products, {
if (haskey (p, "media"))
p = remove (p, "media");
return p;
});
return $[
"add_on_products" : exp,
];
}
global boolean Import (map settings) {
add_on_products = settings["add_on_products"]:[];
modified = false;
if (Mode::config ())
{
foreach (map prod, add_on_products, {
string media = prod["media_url"]:"";
string pth = prod["product_dir"]:"/";
integer src = Pkg::SourceCreate (media, pth);
if (src != -1)
mode_config_sources = add (mode_config_sources, src);
});
}
return true;
}
global void CleanModeConfigSources () {
foreach (integer src, mode_config_sources, {
Pkg::SourceDelete (src);
});
mode_config_sources = [];
}
} // module end