Magento 2 InstallSchema method not executed - database

On my own Magento 2 custom module, I want to install a custom database table. This is the InstallSchema class code:
<?php
namespace MyVendor\MyModule\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class InstallSchema implements InstallSchemaInterface
{
/**
* #inheritdoc
*/
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
$table = $setup->getConnection()
->newTable($setup->getTable('my_table'))
->addColumn(
'greeting_id',
\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
null,
['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
'Greeting ID'
)
->addColumn(
'message',
\Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
255,
['nullable' => false, 'default' => ''],
'Message'
)->setComment("Greeting Message table");
$setup->getConnection()->createTable($table);
$setup->endSetup();
}
}
But the install method is not being executed.
Attached an xdebug session with breakpoints inside the function, never called.
Removed the module line in setup_module database table and re-run bin/magento setup:upgrade
Set the developer mode, disable cache, run a setup:di:compile, still fails.
Any ideas?
I've also tried to use UpdateSchema changing the module version, no luck.
I'm running Magento 2 on a Ubuntu Server virtual box. Permissions on folders are set correctly.

#wildchild great to get it working with another solution. In your question, you did not mention if you are using 2.2.* or 2.3.*, you just stated Magento 2.
I had the same issue after moved from one server to another and upgrading from 2.2 to 2.3 my custom module stop working, the PHP script not creating the table in the database.
I later found out that Magento has changed the music now you have to use declarative schema structure
The Module_Vendor/Module_Name/etc/db_schema.xml file declares a module’s database structure.
See the sample below and you should read more here
Create a table
The following example creates the declarative_table table with four columns. The id_column column is the primary key.
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="declarative_table">
<column xsi:type="int" name="id_column" padding="10" unsigned="true" nullable="false" comment="Entity Id"/>
<column xsi:type="int" name="severity" padding="10" unsigned="true" nullable="false" comment="Severity code"/>
<column xsi:type="varchar" name="title" nullable="false" length="255" comment="Title"/>
<column xsi:type="timestamp" name="time_occurred" padding="10" comment="Time of event"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="id_column"/>
</constraint>
</table>
</schema>
Then run these command // ubuntu
php bin/magento setup:upgrade && php bin/magento cache:flush && php bin/magento cache:clean

In my case the package name was wrong. After replacing by right package name issue is fixed.
namespace Package_Name\Module_Name\Setup;

Please declare and add dependencies in InstallSchema file as given below:-
namespace <Your_Package>\<Your_Module>\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;
Then clear the cache and remove data from generated folder and remove module entry from setup_module.
Make sure that you have defined correct setup version in your module.xml file

Found the solution. It was the module name in the module.xml file. The setup:upgrade installer search for the InstallSchema in a case-sensitive folder path, so if you declare the module as
my_module
it will search on MyVendor/my/module/Setup folder.
I resolve it by replacing the name with:
MyModule
So the installer will search correctly on MyVendor/MyModule/Setup folder.
I found the problem with the "old-way", by placing log messages on the Install.php file on the Magento setup folder.

Related

Configure DDEV with Apache Solr on Drupal 7 for Search API

From #DamienMcKenna in Slack
Having problems creating a Solr instance with a D7 site. I copied the conf files to ~/.ddev/solr/conf but when Solr starts there is no default instance created. I ran ddev stop --remove-data --omit-snapshot and recreated the instance, but the instance still doesn't exist. When I go to the Solr UI to check the system it shows "no cores available", when I try to create one named "dev" it says:
Error CREATEing SolrCore 'dev': Unable to create core [dev] Caused by: Can't find resource 'solrconfig.xml' in classpath or '/opt/solr/server/solr/dev'
I had success on a Drupal 7 project using the example docker-compose-solr.yaml file from version 1.11.0 of DDEV.
Copy https://github.com/drud/ddev/blob/v1.11.0/pkg/servicetest/testdata/services/docker-compose.solr.yaml into your .ddev folder and ensure line 34 matches the solr version you're going to copy from step 2 below, eg solr: 6.6
Copy the files from sites/all/modules/contrib/search_api_solr/solr-conf/6.x/*.* into the .ddev/solr/conf folder.
Download and enable search_api_override module.
Add the following in settings.local.php:
// For ddev only.
$conf['search_api_override_mode'] = 'load';
$conf['search_api_override_servers']['content'] = array(
'name' => 'DDEV: Solr Server',
'options' => array(
'host' => 'solr',
'port' => '8983',
'path' => '/solr/dev',
'http_user' => '',
'http_pass' => '',
'excerpt' => 0,
'retrieve_data' => 1,
'highlight_data' => 0,
'http_method' => 'AUTO',
),
);
Also, ymmv. It may be better to only override the values you need individually… a la:
$conf['search_api_override_servers']['content']['options']['host'] = 'solr';
$conf['search_api_override_servers']['content']['options']['port'] = '8983';
$conf['search_api_override_servers']['content']['options']['host'] = '/solr/dev';
You may need to modify 'content' array index to match whatever you configured in Drupal 7 to be your Solr index's machine name.
Start ddev with ddev start.
NOTE, I place the Search API override values in sites/default/settings.local.php instead of what one would think to be the logical place (sites/default/settings.ddev.php) so as not to interfere with DDEV's own auto-generation of the latter file.
It would be cool if DDEV did this automatically in settings.ddev.php similar to how the DB service settings work, but AFAICT this level of integration is not there and likely never will be for Drupal 7. Firstly, because you need an additional module (search_api_override) that may or may not be present, and secondly because users have the ability to name their Solr server whatever they want, so it would be hard to automate that. E.g. $conf['search_api_override_servers']['content'] could be anything like: $conf['search_api_override_servers']['foo'].

Problems accessing datasource in Karaf 4 from Apache Camel

I've created a datasource in Karaf 4 (ServiceMix 7) that works from the karaf console - I can list tables, execute queries and so on.
My issue is when I try to use it from my Camel route.
Excerpt from my blueprint:
...
<reference filter="(osgi.jndi.service.name=jdbc/erp)" id="erpDataSource" interface="javax.sql.DataSource"/>
...
<to id="erpSelectQuery" uri="jdbc:erpDataSource"/>
...
It finds my dataSource but the blueprint can't start due to:
"java.lang.IllegalArgumentException: connectionFactory must be specified"
My datasource was created using:
jdbc:ds-create -dbName erp -dt DataSource -dn mysql -u erp -dc com.mysql.jdbc.Driver -p pre jdbc/erp
I'm at loss here
I have never done it via the jdbc command syntax I followed the guides from the Ops4J Wiki On Datasource creation which I like for one reason alone, this method creates a simple text file that can be administered by not just a Java developer, i.e. it is easier to modify and troubleshoot.
For the sake of not subjecting my answer to link rot I will just outline the procedure here.
Create a datasource configuration file(simple text file) in /servicemixhome/etc with the following naming convention org.ops4j.datasource-give_your_datasource_a_name.cfg .
In the config file configure the appropriate settings an example of mine looks like this:
osgi.jdbc.driver.class = com.mysql.jdbc.Driver
databaseName=dhData
user=foo
url=jdbc:mysql://192.199.199.199:3306/dhData
password=somepassword
dataSourceName=myDSName
Make sure you installed the ops4j required features:
feature:install pax-jdbc-mysql pax-jdbc-config
Now list the datasources using the following syntax:
karaf#root()> service:list javax.sql.DataSource
This will echo something like the list below back.
[javax.sql.DataSource]
----------------------
osgi.jdbc.driver.class = com.mysql.jdbc.Driver
databaseName=dhData
user=foo
url=jdbc:mysql://192.199.199.199:3306/dhData
password=somepassword
dataSourceName=myDSName
Provided by :
OPS4J Pax JDBC Config (216)
At this point you can reference the datasource usign an osgi filter in the blueprint.xml with the following syntax:
<reference filter="(&(objectClass=javax.sql.DataSource)(dataSourceName=myDSName ))" id="myData" interface="javax.sql.DataSource"/>
Then to reference this as property of a bean for example you could do the following:
<bean class="foo.bar" id="ImsCbrEventsBean">
<property name="dataSource" ref="myData"/>
</bean>
Keep in mind this creates a singular connection to a database and you should really create a connection pool.
This can be done by installing the pax-jdbc-pool-dbcp2 feature or any of the other connection pools but use only one at a time, then modifying the datasource config file to carry appropriate information like the example below:
osgi.jdbc.driver.name = mysql
databaseName=dhData
user=foo
url=jdbc:mysql://192.199.199.199:3306/dhData
password=somepassword
dataSourceName=myDSName
jdbc.pool.maxTotal=32
jdbc.pool.blockWhenExhausted=true
jdbc.pool.lifo=false
jdbc.pool.maxIdle=24
jdbc.pool.maxWaitMillis=5000
jdbc.pool.minEvictableIdleTimeMillis=1800000
jdbc.pool.minIdle=16
jdbc.pool.numTestsPerEvictionRun=3
jdbc.pool.softMinEvictableIdleTimeMillis=-1
jdbc.pool.testOnBorrow=true
jdbc.pool.testOnCreate=true
jdbc.pool.testOnReturn=true
jdbc.pool.testWhileIdle=true
jdbc.pool.timeBetweenEvictionRunsMillis=3600000

How to Insert Value to Joomla 2.5 DB with Custom Plugin

I am trying to create a joomla plugin which will insert a value into a database table for a Joomla component. I have successfully built and installed the plugin through the Joomla Extension Manager, but when I enable the plugin, I am not seeing the value inserted in the database table. Here is my php code from the plugin to review:
<?php
######################################################################
# Outkast Plugin #
######################################################################
// no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
jimport( 'joomla.plugin.plugin');
jimport( 'joomla.html.parameter');
class plgSystemOutkastplugin extends JPlugin
{
function plgSystemOutkastplugin()
{
$db = JFactory::getDbo();
$name = "http://www.youtube.com/embed/PWgvGjAhvIw?rel=0";
$columns = array('title', 'link_youtube');
$name = "http://www.youtube.com/embed/PWgvGjAhvIw?rel=0";
$values = array($db->quote('Outkast'), $db->quote($name));
$query->insert($db->quoteName('#__mycomponent_outkastvideos'))
->columns($db->quoteName($columns))
->values(implode(',', $values));
$db->setQuery($query);
$db->query();
return true;
}
}
?>
Edit Update 11/07/13:
I am adding the xml file from the plugin to help give some more context to the question. Ultimately I have the combo of the php file above and the xml below to create the entire plugin package. When I install the component on a Joomla 2.5 site, it installs with no errors. Then, when I enable the plugin, I get two instances of the video inserted into the database. When I disable the plugin I will get one instance of the video inserted into the database. This is not what I am after.
Ideally, when the user installs the plugin, I would like it to automatically enable the plugin. If the plugin is enabled, then the video will show up in the database. If the plugin is disabled, then the video will be not show up in the database. Here is the current xml code:
<?xml version="1.0" encoding="utf-8"?>
<install version="1.7" type="plugin" group="system">
<name>Add Outkast</name>
<author>My Name</author>
<creationDate>November 2013</creationDate>
<copyright>Copyright (C) 2013 All rights reserved.</copyright>
<license>http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL</license>
<authorEmail>my#email.com</authorEmail>
<authorUrl>myURL.com</authorUrl>
<version>1.7</version>
<description>This Plugin adds Outkast's Video Hey Ya.</description>
<files>
<filename plugin="outkastplugin">outkastplugin.php</filename>
</files>
</install>
Right, I went for a different approach with this. I did it using an SQL file which gets executed upon installation.
Here is what everything looks like:
Structure:
file - outkastplugin.xml
file - outkastplugin.php
folder - sql
>> file - install.mysql.utf8.sql
outkastplugin.xml
<?xml version="1.0" encoding="utf-8"?>
<extension version="2.5" type="plugin" group="system" method="upgrade">
<name>Add Outkast</name>
<author>My Name</author>
<creationDate>November 2013</creationDate>
<copyright>Copyright (C) 2013 All rights reserved.</copyright>
<license>http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL</license>
<authorEmail>my#email.com</authorEmail>
<authorUrl>myURL.com</authorUrl>
<version>1.7</version>
<description>This Plugin adds Outkast's Video Hey Ya.</description>
<files>
<filename plugin="outkastplugin">outkastplugin.php</filename>
<folder>sql</folder>
</files>
<install>
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
</sql>
</install>
</extension>
outkastplugin.php:
<?php
defined( '_JEXEC' ) or die( 'Restricted access' );
jimport( 'joomla.plugin.plugin');
jimport( 'joomla.html.parameter');
class plgSystemOutkastplugin extends JPlugin
{
function plgSystemOutkastplugin()
{
}
}
?>
install.mysql.utf8.sql:
CREATE TABLE IF NOT EXISTS `#__mycomponent_outkastvideos` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`link_youtube` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
INSERT INTO `#__mycomponent_outkastvideos` (`title`, `link_youtube`) VALUES ('Outkast', 'http://www.youtube.com/embed/PWgvGjAhvIw?rel=0');
I have sent you the full zip package via e-mail.
Hope this helps :)

Replicate Behavior of a Batch File in a Custom Action

I'm creating a wix installer at work, and I need to be able to replicate the behavior of this batch file in a custom action:
start /d "C:\Program Files (x86)\Remindex" INSTSRV.EXE RemindexNP
"C:\Program Files (x86)\Remindex\SRVANY.EXE"
I am trying to create a service using a normal windows application, which SRVANY.EXE can do. This batch file runs fine normally, but I can't seem to get a custom action to do the same thing. I have tried this:
<CustomAction Id="RunNP" FileKey="FILE_INSTALLFOLDER_INSTSRVEXE"
ExeCommand="RemindexNP [INSTALLFOLDER]SRVANY.EXE" Execute="commit" Return="ignore"/>
This custom action doesn't cause any errors that I can see in the log file, but I don't think instsrv.exe is accepting the parameters I'm passing in ExeCommand. I know that instsrv.exe and srvany.exe exist because I'm running the custom action before InstallFinalize.
Anyone know what's wrong with my custom action?
I would prefer not to include an actual batch file in my install folder, as it would have no reason to be there besides being run on install. I have tried including one in the installer, but I don't know how to reference the install directory. When I use %cd%, it just references the system folder for some reason.
I tried using ServiceInstall and ServiceControl elements, but the installer gets stuck on "starting services". Here is my component:
<Component Id="CMP_RemindexNP.exe" Guid="{3FB99890-752D-4652-9412-72230695A520}">
<File Id="FILE_INSTALLFOLDER_RemindexNPEXE" Source="RemindexNP.exe" KeyPath="yes"/>
<RegistryKey Root="HKLM" Key="SYSTEM\CurrentControlSet\Services\RemindexNP\Parameters">
<RegistryValue Id="rg_remNP1" Action="write" Name="AppDirectory" Value="[INSTALLFOLDER]" Type="string"/>
<RegistryValue Id="rg_remNP2" Action="write" Name="Application" Value="[INSTALLFOLDER]RemindexNP.exe" Type="string"/>
</RegistryKey>
<ServiceInstall DisplayName="RemindexNP" Id="srv_remNP" Name="RemindexNP" Start="auto" Type="shareProcess" ErrorControl="ignore"/>
<ServiceControl Id="srvc_remNP" Name="RemindexNP" Remove="both" Start="install" Stop="uninstall" Wait="no"/>
</Component>
And my log:
Action 17:15:08: StartServices. Starting services
Action start 17:15:08: StartServices.
StartServices: Service: Starting services
Action ended 17:15:08: StartServices. Return value 1.
Any suggestions would be greatly appreciated.
This is a trick question as you don't need a custom action. The srvany.exe acts as a service host and as such can be authored into your installer using the Directory, Component, File, ServiceInstall and (if desired) ServiceControl elements.

Can I force the installer project to use the .config file from the built solution instead of the original one?

I am using the solution to this question in order to apply configuration changes to App.config in a Winforms project. I also have an installer project for the project that creates an installable *.msi file. The problem is, the config file bundled in the installers is the original, un-transformed config file. So we're not getting the production connection strings in the production installer even though the config file for the built winforms project has all the correct transformations applied.
Is there any way to force the installer project to use the output of project build?
First of all: it is impossible to make the Setup Project point to another app.config file by using the Primary output option. So my solution is going to be a work around. I hope you find it useful in your situation.
Overview:
The basic idea is:
Remove the forced app.config from the Setup Project;
Add a file pointing to the app.config, manually;
Use MSBuild to get into the vdproj file, and change it to match the real output of the transformed app.config.
Some drawbacks are:
The setup project only gets updated, if the project it deploys build. ahhh... not a real drawback!
You need MSBuild 4.0... this can also be worked around!
Need a custom Task, called FileUpdate... it is open source and has installer.
Lets Work:
1) Go to your Setup Project, and select the Primary Output object, right click and go to Properties. There you will find the Exclude Filter... add a filter for *.config, so it will remove the hard-coded app.config.
2) Right click your Setup Project in the Solution Explorer -> Add -> File... select any file that ends with .config.
3) Download MSBuild Community Tasks Project, I recomend the msi installer.
4) Unload your project (the csproj) and replace the code from the other question with this one:
Code:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
<!-- Generate transformed app config in the intermediate directory -->
<TransformXml Source="app.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="app.$(Configuration).config" />
<!-- Force build process to use the transformed configuration file from now on. -->
<ItemGroup>
<AppConfigWithTargetPath Remove="app.config" />
<AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
<TargetPath>$(TargetFileName).config</TargetPath>
</AppConfigWithTargetPath>
</ItemGroup>
<PropertyGroup>
<SetupProjectPath>$(MSBuildProjectDirectory)\$(IntermediateOutputPath)$(TargetFileName).config</SetupProjectPath>
</PropertyGroup>
<!-- Change the following so that this Task can find your vdproj file -->
<FileUpdate Files="$(MSBuildProjectDirectory)\..\Setup1\Setup1.vdproj"
Regex="(.SourcePath. = .8:).*\.config(.)"
ReplacementText="$1$(SetupProjectPath.Replace(`\`,`\\`))$2" />
<FileUpdate Files="$(MSBuildProjectDirectory)\..\Setup1\Setup1.vdproj"
Regex="(.TargetName. = .8:).*\.config(.)"
ReplacementText="$1$(TargetFileName).config$2" />
</Target>
5) The previous code must be changed, so that it can find your vdproj file. I have placed a comment in the code, indicating where you need to make the change.
Now, everytime you build your main project, the MSBuild will change the Setup project, so that it uses the correct app.config file. It may have drawbacks, but this solution can be polished and become better. If you need leave a comment, and I'll try to respond ASAP.
Resources I Used
MSBuild 4.0 is needed because I need to use String's Replace function, to replace single "\" to double "\" in the path. See
MSBuild Property Functions for details about using function in MSBuild.
I learned about the FileUpdate Task in this other question. The official project is MSBuild Community Tasks Project.
These two topics were important to my findings:
Trying to include configuration specific app.config files in a setup project
Problems with setup project - am I thick?
Another solution I've found is not to use the transformations but just have a separate config file, e.g. app.Release.config. Then add this line to your csproj file.
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<AppConfig>App.Release.config</AppConfig>
</PropertyGroup>
This will force the deployment project to use the correct config file when packaging.
I combined the best of the following answers to get a fully working solution without using any external tools at all:
1. Setup App.Config transformations
Source: https://stackoverflow.com/a/5109530
In short:
Manually add additional .config files for each build configuration and edit the raw project file to include them similar to this:
<Content Include="App.config" />
<Content Include="App.Debug.config" >
<DependentUpon>App.config</DependentUpon>
</Content>
<Content Include="App.Release.config" >
<DependentUpon>App.config</DependentUpon>
</Content>
Then include the following XML at the end of the project file, just before the closing </project> tag:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
<TransformXml Source="app.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="app.$(Configuration).config" />
<ItemGroup>
<AppConfigWithTargetPath Remove="app.config" />
<AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
<TargetPath>$(TargetFileName).config</TargetPath>
</AppConfigWithTargetPath>
</ItemGroup>
</Target>
Finally edit the additional .config files to include the respective transformations for each build configuration:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<!-- transformations here-->
</configuration>
2. Include the appropriate .config in the setup project
First, add a command in the postbuild event of your main project to move the appropriate transformed .config file to a neutral location (e.g. the main bin\ directory):
copy /y "$(TargetDir)$(TargetFileName).config" "$(ProjectDir)bin\$(TargetFileName).config"
(Source: https://stackoverflow.com/a/26521986)
Open the setup project and click the "Primary output..." node to display the properties window. There, add an ExludeFilter "*.config" to exclude the default (untransformed) .config file.
(Source: https://stackoverflow.com/a/6908477)
Finally add the transformed .config file (from the postbuild event) to the setup project (Add > File).
Done.
You can now freely add build configurations and corresponding config transforms and your setup project will always include the appropriate .config for the active configuration.
I accomplished this in a different manner with no external tools:
I added a post-build event that copied the target files to a 'neutral' directory (the root of the /bin folder in the project) and then added this file to the .vdproj. The deployment project now picks up whatever the latest built version is:
Post Build Command:
copy /y "$(TargetDir)$(TargetFileName).config" "$(ProjectDir)bin\$(TargetFileName).config"
This worked for what I needed without any external tools, and works nicely with SlowCheetah transformations.
Based off Alec's answer, here is a similar element that you can use along with the transformations and still get their full benefit:
<ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Content Include="$(OutputPath)$(AssemblyName).dll.config">
<InProject>false</InProject>
<Link>$(AssemblyName).dll.config</Link>
</Content>
</ItemGroup>
This way, you can use the SlowCheetah transforms or the built-in ones to transform your .config file, and then go into your Visual Studio Deployment Project (or other) and include the Content from the affected project in your Add -> Project Output... page easily, with minimal changes.
None of the above solutions or any articles worked for me in deployment/setup project. Spent many days to figure out the right solution. Finally this approach worked for me.
Pre requisites
I've used utility called cct.exe to transform file explicitly. You can download from here
http://ctt.codeplex.com/
I've used custom installer in setup project to capture installation events.
Follow these steps to achieve app config transformation
1) Add your desired config files to your project and modify your .csproj file like these
<Content Include="app.uat.config">
<DependentUpon>app.config</DependentUpon>
</Content>
<Content Include="app.training.config">
<DependentUpon>app.config</DependentUpon>
</Content>
<Content Include="app.live.config">
<DependentUpon>app.config</DependentUpon>
</Content>
I've added them as content so that they can be copied to output directory.
2) Add cct.exe to your project which you downloaded.
3) Add custom installer to your project which should look like this
[RunInstaller(true)]
public partial class CustomInstaller : System.Configuration.Install.Installer
{
string currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string[] transformationfiles = Directory.GetFiles(Path.GetDirectoryNam(Assembly.GetExecutingAssembly().Location), "app.*.config");
public CustomInstaller()
{
InitializeComponent();
// Attach the 'Committed' event.
this.Committed += new InstallEventHandler(MyInstaller_Committed);
this.AfterInstall += new InstallEventHandler(CustomInstaller_AfterInstall);
}
void CustomInstaller_AfterInstall(object sender, InstallEventArgs e)
{
try
{
Directory.SetCurrentDirectory(currentLocation);
var environment = Context.Parameters["Environment"];
var currentconfig = transformationfiles.Where(x => x.Contains(environment)).First();
if (currentconfig != null)
{
FileInfo finfo = new FileInfo(currentconfig);
if (finfo != null)
{
var commands = string.Format(#"/C ctt.exe s:yourexename.exe.config t:{0} d:yourexename.exe.config ", finfo.Name);
using (System.Diagnostics.Process execute = new System.Diagnostics.Process())
{
execute.StartInfo.FileName = "cmd.exe";
execute.StartInfo.RedirectStandardError = true;
execute.StartInfo.RedirectStandardInput = true;
execute.StartInfo.RedirectStandardOutput = true;
execute.StartInfo.UseShellExecute = false;
execute.StartInfo.CreateNoWindow = true;
execute.StartInfo.Arguments = commands;
execute.Start();
}
}
}
}
catch
{
// Do nothing...
}
}
// Event handler for 'Committed' event.
private void MyInstaller_Committed(object sender, InstallEventArgs e)
{
XmlDocument doc = new XmlDocument();
var execonfigPath = currentLocation + #"\yourexe.exe.config";
var file = File.OpenText(execonfigPath);
var xml = file.ReadToEnd();
file.Close();
doc.LoadXml(FormatXmlString(xml));
doc.Save(execonfigPath);
foreach (var filename in transformationfiles)
File.Delete(filename);
}
private static string FormatXmlString(string xmlString)
{
System.Xml.Linq.XElement element = System.Xml.Linq.XElement.Parse(xmlString);
return element.ToString();
}
}
Here I am using two event handlers CustomInstaller_AfterInstall in which I am loading correct config file and transforming .
In MyInstaller_Committed I am deleting transformation files which we don't need on client machine once we apply has been applied. I am also indenting transformed file because cct simply transforms elements were aligned ugly.
4) Open your setup project and add project output content file so that setup can copy config files like app.uat.config,app.live.config etc into client machine.
In previous step this snippet will load all available config files but we need supply right transform file
string[] transformationfiles = Directory.GetFiles(Path.GetDirectoryNam
(Assembly.GetExecutingAssembly().Location), "app.*.config");
For that I've added UI dialog on setup project to get the current config. The dialog gives options for user to select environment like "Live" "UAT" "Test" etc .
Now pass the selected environment to your custom installer and filter them.
It will become lengthy article if I explain on how to add dialog,how to set up params etc so please google them. But idea is to transform user selected environment.
The advantage of this approach is you can use same setup file for any environment.
Here is the summary:
Add config files
Add cct exe file
Add custom installer
Apply transformation on exe.config under after install event
Delete transformation files from client's machine
Modify setup project in such a way that
set up should copy all config files(project output content) and cct.exe into output directory
configure UI dialog with radio buttons (Test,Live,UAT..)
pass the selected value to custom installer
Solution might look lengthy but have no choice because MSI always copy app.config and doesn't care about project build events and transformations. slowcheetah works only with clickonce not setup project
The question is old, but the following could still help many folks out there.
I would simply use Wix WiFile.exe to replace the concerned file in the msi this way (for the sake of this example, we call your msi yourPackage.msi):
Step 1. From command prompt run: WiFile.exe "yourPackage.msi" /x "app.exe.config."
The above will extract the "wrong" app.exe.config file from the msi and place it the same directory as your msi;
Step 2. Place the new (prod) config file (must have the same name as the extracted file: app.exe.config) in same location as your msi;
This means that you are overwritting the app.exe.config that has just been extracted in step 1 above, with your new (production config file);
Step 3. From command prompt run: WiFile.exe "yourPackage.msi" /u "app.exe.config."
THAT'S ALL!
The above can be done in a few seconds. You could automate the task if you wanted, for instance, by running it as batch or else.
After running step 3 above, your msi will contain the new config file, which will now be installed at your clients' when they run the setup.

Resources