Extracting files from merge module - installation

All I want is a command-line tool that can extract files from a merge module (.msm) onto disk. Said differently, I want the same "administrative install" functionality that is possible for an MSI:
msiexec /a myProduct.msi TARGETDIR="C:\myInstallation" /qn
The above only works on an msi (near as I can tell). So to get the same effect for a merge module, I'm trying msidb.exe and orca.exe The documentation for orca states:
Many merge module options can be
specified from the command line...
Extracting Files from a Merge Module
Orca supports three different methods
for extracting files contained in a
merge module. Orca can extract the
individual CAB file, extract the files
into a module tree and extract the
files into a source image once it has
been merged into a target database...
Extracting Files
To extract the individual files from a
merge module, use the
... -x ... option on the
command line, where is the
desired path to the new directory
tree.
The specified path is used as the root
path for the extracted files. All
files are extracted from the CAB file
embedded in the module and placed in
the specified path. The directory
layout for the extracted files is
based on the directory tree of the
merge module.
It sounds like what I need. But when I try it, orca simply opens up an editor (with info on the msm I specified) and then does nothing. I've tried a variety of command lines, usually starting with this:
orca -x theDirectory theModule.msm
I use "theDirectory" as whatever empty folder I want. Like I said - it didn't do anything.
Then I tried msidb, where a couple of attempts I've made look like this:
msidb -d theModule.msm -w {storage}
msidb -d theModule.msm -x MergeModule.CABinet
In the first case, I don't know what to put for {storage}. In the second case, it turns out that the literal string "MergeModule.CABinet" is necessary (it is a reserved name). However, the extracted cabinet does not preserve the file hierarchy or "normal" file names; so I can't use it for my purposes.
Can someone explain what I'm doing wrong with the command line options? Is there any other tool that can do this?

You can use the decompiler tool included with WiX (called Dark) to decompile the merge module and extract the files:
dark.exe myMergeModule.msm -x "path_to_extracted_files"
The files will get extraced to the path specified in the -x parameter.
Note: The files will get extracted using the names specified in the File table of the installation database, which may not actually be the file names used when the files actually get installed. If you need extract the files using the actual file names, see my other answer to this question: Extracting files from merge module

I just had to do this by creating a blank msi and then use Orca to attempt to merge the module into my msi and then extract the files.
Create a blank .msi. I used WiX 3.6 to create the .msi and below is the minimal source. I named it "blank.msi".
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="blank" Language="1033" Version="1.0.0.0" Manufacturer="blank" UpgradeCode="298878d0-5e7b-4b2e-84f9-45bb66541b10">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MediaTemplate />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder"/>
</Directory>
<ComponentGroup Id="ProductComponents" Directory="ProgramFilesFolder" />
<Feature Id="ProductFeature" Title="blank" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
</Wix>
Use Orca to extract the files from the merge module.
orca -m "myModule.msm" -f ProductFeature -x .\xdir blank.msi
The files will be extracted to the directory specified by the -x parameter (in this case .\xdir).
Note that the value for the -f parameter "ProductFeature" matches the name of the feature specified in msi file above.

The DeploymentToolsFoundation class library in WiX, has an InstallPackage class with an ExtractFiles() method that should do just what you want, but fails for Merge Modules. This appears to be a bug.
The following PowerShell script, which uses DTF to access the CAB in the mergemodule, should do what you want. Apologies if the scripting is a bit wonky, I'm new to PowerShell.
[Reflection.Assembly]::LoadFrom("[InsertPath]\Microsoft.Deployment.WindowsInstaller.dll")
function ExtractMSM([string]$file, [string]$targetDir)
{
write-host "Extracting files from merge module: "$file
if(![IO.Directory]::Exists($targetDir)) { new-item -type directory -path $targetDir }
$cabFile = join-path $targetDir "temp.cab"
if([IO.File]::Exists($cabFile)) { remove-item $cabFile }
$db = new-object Microsoft.Deployment.WindowsInstaller.DataBase($file, [Microsoft.Deployment.WindowsInstaller.DataBaseOpenMode]::ReadOnly)
$view = $db.OpenView("SELECT `Name`,`Data` FROM _Streams WHERE `Name`= 'MergeModule.CABinet'")
$view.Execute()
$record = $view.Fetch()
$record.GetStream(2, $cabFile)
$view.Dispose()
expand -F:* $cabFile $targetDir
remove-item $cabFile
$extractedFiles = get-childitem $targetDir
$hashFiles = #{}
foreach($extracted in $extractedFiles)
{
try
{
$longName = $db.ExecuteScalar("SELECT `FileName` FROM `File` WHERE `File`='{0}'", $extracted.Name)
}
catch
{
write-host "$($extracted.Name) is not in the MSM file"
}
if($longName)
{
$longName = $LongName.SubString($LongName.IndexOf("|") + 1)
Write-host $longName
#There are duplicates in the
if($hashFiles.Contains($longName))
{
write-host "Removing duplicate of $longName"
remove-item $extracted.FullName
}
else
{
write-host "Rename $($extracted.Name) to $longName"
$hashFiles[$longName] = $extracted
$targetFilePath = join-path $targetDir $longName
if([IO.File]::Exists($targetFilePath)) {remove-item $targetFilePath}
rename-item $extracted.FullName -NewName $longName
}
}
}
$db.Dispose()
}

I had a similar problem, but I went at it from a different direction.
I installed InstallSheild Express that came with an earlier version of Visual Studio, created a new project, but I only added the MSM file that I required.
After compiling and running my new install I was able to retrieve the files that the MSM file contained.

MSI2XML

Related

How to show file and folder names along with terminal icons without showing current directory?

I want to remove the space/ area taken up in showing the
Directory: C:\Users\varun\Desktop\Projects\advanced-react-patterns-v2
when I run the command:
Get-ChildItem | Format-Wide
Additional details:
Using Windows Terminal & Powershell
Font used in the screenshot: TerminessTTF NF
Used Terminal-Icons
Note: The command
Get-ChildItem -Name
failed to show the terminal icons which kind of my main goal here.
When you are using a Format-* command you are using the default formatting output for the File and Directory objects, which groups files by directory - hence the directory name at the top.
If you wanted to by pass this, you would have to write your own format.ps1xml file and then add the formatting to your output.
$files = Get-ChildItem
foreach ($file in $files) {
$file.PSObject.TypeNames.Insert(0,'Custom.Output.Type')
$file
}
Small sample of XML for the specified Typename, customise as you wish.
<View>
<Name>CustomFileFormatting</Name>
<ViewSelectedBy>
<TypeName>Custom.Output.Type</TypeName>
</ViewSelectedBy>
<TableControl>
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>FullName</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>FSObject</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>

Archive all files of a certain type recursively from powershell

Is there a way to use Compress-Archive script, that when run from a path:
archives all files matching a wildcard filter (*.doc, for example)
archives such files in the current folder and all children folders
save the relative folder structure (the option to use relative or absolute would be good, though)
I am having trouble have it accomplish all three of these at once.
Edit:
The following filters and recurses, but does not maintain folder structure
Get-ChildItem -Path ".\" -Filter "*.docx" -Recurse |
Compress-Archive -CompressionLevel Optimal -DestinationPath "$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip"
This item does not recurse:
Compress-Archive -Path "$pwd\*.docx" -CompressionLevel Optimal -DestinationPath "$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip"
At some point I had a command that would recurse but not filter, but can't get back to it now.
Unfortunately, Compress-Archive is quite limited as of Windows PowerShell v5.1 / PowerShell Core 6.1.0:
The only way to preserve a subdirectory tree is pass a directory path to Compress-Archive.
Unfortunately, doing so provides no inclusion/exclusion mechanism to only select a subset of files.
Additionally, the resulting archive will internally contain a single root directory named for the input directory (e.g., if you pass C:\temp\foo to Compress-Archive, the resulting archive will contain a single foo directory containing the input directory's subtree - as opposed to containing C:\temp\foo's content at the top level).
There is no option to preserve absolute paths.
A cumbersome work around is to create a temporary copy of your directory tree with only the files of interest (Copy-Item -Recurse -Filter *.docx . $env:TEMP\tmpDir; Compress-Archive $env:TEMP\tmpDir out.zip - note that empty dirs. will be included)
Given that you'll still invariably end up with a single root directory named for the input directory inside the archive, even that may not work for you - see the alternatives at the bottom.
You may be better off with alternatives:
Use the .NET v4.5+ [System.IO.Compression.ZipFile] and [System.IO.Compression.ZipFileExtensions] types directly.
In Windows PowerShell, unlike in PowerShell Core (v6+), you most load the relevant assembly manually with Add-Type -AssemblyName System.IO.Compression.FileSystem - see below.
Use an external programs such as 7-Zip
Solving the problem with direct use of the .NET v4.5+ [System.IO.Compression.ZipFile] class:
Note:
In Windows PowerShell, unlike in PowerShell Core, you most load the relevant assembly manually with Add-Type -AssemblyName System.IO.Compression.FileSystem.
Because PowerShell doesn't support implicit use of extension methods as of Windows PowerShell v5.1 / PowerShell Core 6.1.0, you must make explicit use of the [System.IO.Compression.ZipFileExtensions] class as well.
# Windows PowerShell: must load assembly System.IO.Compression.FileSystem manually.
Add-Type -AssemblyName System.IO.Compression.FileSystem
# Create the target archive via .NET to provide more control over how files
# are added.
# Make sure that the target file doesn't already exist.
$archive = [System.IO.Compression.ZipFile]::Open(
"$pwd\doc.archive-$(Get-Date -f yyyyMMdd.hhmmss).zip",
'Create'
)
# Get the list of files to archive with their relative paths and
# add them to the target archive one by one.
$useAbsolutePaths = $False # Set this to true to use absolute paths instead.
Get-ChildItem -Recurse -Filter *.docx | ForEach-Object {
# Determine the entry path, i.e., the archive-internal path.
$entryPath = (
($_.FullName -replace ([regex]::Escape($PWD.ProviderPath) + '[/\\]'), ''),
$_.FullName
)[$useAbsolutePaths]
$null = [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive,
$_.FullName,
$entryPath
)
}
# Close the archive.
$archive.Dispose()

ProjectItems.AddFromFile Adds File to Pending Changes

As part of my nuget package, I have an install.ps1 powershell script that I am using to add a reference file to the project (a couple text documents) from the package's tools folder.
Everything is working great, except that when the files are referenced in a TFS solution, they are added to the Team Explorer Pending Changes. How can I remove them from pending changes (or keep them from ever showing up)? I don't want these checked into TFS, since the packages folder shouldn't be there in the first place.
Here's my install.ps1 script:
param($installPath, $toolsPath, $package, $project)
#Add reference text files to the project and opens them
Get-ChildItem $toolsPath -Filter *.txt |
ForEach-Object {
$projItem = $project.ProjectItems.AddFromFile($_.FullName)
If ($projItem -ne $null) {
$projItem.Properties.Item("BuildAction").Value = 0 # Set BuildAction to None
}
}
If you're using local workspaces (TFS 2012+) you can use the .tfignore file to exclude local folders and files from appearing in the Pending Changes page in Team Explorer.
You can configure which kinds of files are ignored by placing text file called .tfignore in the folder where you want rules to apply.
.tfignore file rules
The following rules apply to a .tfignore file:
- \# begins a comment line
- The \* and ? wildcards are supported.
- A filespec is recursive unless prefixed by the \\ character.
- ! negates a filespec (files that match the pattern are not ignored)
.tfignore file example
######################################
# Ignore .cpp files in the ProjA sub-folder and all its subfolders
ProjA\*.cpp
#
# Ignore .txt files in this folder
\*.txt
#
# Ignore .xml files in this folder and all its sub-folders
*.xml
#
# Ignore all files in the Temp sub-folder
\Temp
#
# Do not ignore .dll files in this folder nor in any of its sub-folders
!*.dll
Details: https://www.visualstudio.com/docs/tfvc/add-files-server#customize-which-files-are-ignored-by-version-control
I finally figured out how to do it using tf.exe. Calling tf vc undo with the full filename will undo pending changes for those files. And if the folder isn't tied to TFS, no harm done. It just continues on.
This implementation does require VS 2015 to be installed (due to the hardcodes path to the IDE folder), so I'm looking for a better way to obtain the IDE path of the currently loaded IDE. For now though, this solves my current issue.
param($installPath, $toolsPath, $package, $project)
$idePath = "$env:VS140COMNTOOLS..\IDE"
$tfPath = "$idePath\tf.exe"
Get-ChildItem $toolsPath -Filter *.txt |
ForEach-Object {
$projItem = $project.ProjectItems.AddFromFile($_.FullName)
If ($projItem -ne $null) {
$projItem.Properties.Item("BuildAction").Value = 0 # Set BuildAction to None
$filename = $_.FullName
& $tfPath vc undo `"$filename`" # Remove File from TFS Pending Changes, as AddFromFile can automatically add it
}
}

How can I force files in my project so that they always copy to output directory

I am using a visual studio c# library project to contain static resources that are needed as deployment artifacts. (in my case SQL files that are run with a combination of RoundhousE and Octopus deploy). By convention all files in the project must have their properties set so that the "Build action" is "Content" and "Copy to output directory" is "Copy always".
If someone on the team adds a file but forgets to set these properties we see deployment errors. This is usually picked up in an internal environment, but I was hoping to find a way to enforce this in the CI build.
So is there a way to either fail the build or better still override these properties during the build with an MS Build task? Am I tackling this the wrong way? Any suggestions welcomed.
You are going to have to parse the project files and check for Content without CopyToOutputDirectory set to Always, I doubt there is another way.
That can be done using whatever scripting language you want, or you could even write a small C# tool that uses the classes from the Microsoft.Build.Evaluation namespace. Here is a possible PowerShell implementation - the hardest part is getting the regexes right. First one checks for Content without any metadata, second one for Content where CopyToOutputDirectory does not start with "A" (which I assume should be "Always", no idea how to match that whole word).
FindBadContentNodes.ps1 :
param([String]$inputDir)
Function FindBadContent()
{
$lines = Get-Content $input
$text = [string]::Join( "`n", $lines )
if( $text -match "<Content Include.*/>" -Or
$text -match "<Content Include.*`n\s*<CopyToOutputDirectory>[^A]\w*<.*" )
{
"Found file with bad content node"
exit 1
}
}
Get-ChildItem -Recurse -Include *.csproj -Path $inputDir | FindBadContent
Call this from MsBuild:
<Target Name="FindBadContentNodes">
<Exec Command="Powershell FindBadContentNodes.ps1 -inputDir path\to\sourceDir"/>
</Target>
Note you mention or better still override these properties during the build. I'd stay away from such a solution: you're just burying the problem and relying on the CI to produce correct builds, so local builds using just VS would not be the same. Imo making the build fail is better, especially since most CI systems have a way of notifying the developper that is responsible anyway so the fix should be applied quickly.
Another possibility would be to have the CI apply the fix and then commit the changes so at least everyone has the correct version.
IIRC there is a way in Visual Studio to set a file extension to do certain things on default, much like .config files will always set to content and copy to output directory.
So one could do the same with .sql files (and other files that they would want to be set up this way). A quick search brought me to this: http://blog.andreloker.de/post/2010/07/02/Visual-Studio-default-build-action-for-non-default-file-types.aspx
The relevant parts:
The default build action of a file type can be configured in the
registry. However, instead of hacking the registry manually, we use a
much better approach: pkgdef files (a good article about pkgdef
files). In essence, pkdef are configuration files similar to .reg
files that define registry keys and values that are automatically
merged into the correct location in the real registry. If the pkgfile
is removed, the changes are automatically undone. Thus, you can safely
modify the registry without the danger of breaking anything – or at
least, it’s easy to undo the damage.
Finally, here’s an example of how to change the default build action
of a file type:
1: [$RootKey$\Projects{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\FileExtensions.spark]
2: "DefaultBuildAction"="Content" The Guid in the key refers to project type. In this case, “{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}” means “C# projects”. A rather comprehensive list of project type guids can be found here. Although it does not cover Visual Studio 2010 explicitly, the Guids apply to the current version as well. By the way, we can use C# as the project type here, because C# based MVC projects are in fact C# projects (and web application projects). For Visual Basic, you’d use “{F184B08F-C81C-45F6-A57F-5ABD9991F28F}” instead.
$RootKey$ is in abstraction of the real registry key that Visual
Studio stores the configuration under:
HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0_Config (Note:
Do not try to manually edit anything under this key as it can be
overwritten at any time by Visual Studio).
The rest should be self explanatory: this option sets the default
build action of .spark files to “Content”, so those files are included
in the publishing process.
All you need to do now is to put this piece of text into a file with
the extension pkgdef, put it somewhere under
%PROGRAMFILES(x86)%\Microsoft Visual Studio
10.0\Common7\IDE\Extensions (on 64-bit systems) or %PROGRAMFILES(x86)%\Microsoft Visual Studio
10.0\Common7\IDE\Extensions (on 32-bit systems) and Visual Studio will load and apply the settings automatically the next time it starts. To
undo the changes, simply remove the files.
Finally, I’ve attached a bunch of pkgdef files that are use in
production that define the “Content” default Build Action for C# and
VB projects for .spark, .brail, .brailjs and .less files respectively.
Download them, save them somewhere in the Extensions folder and you’re
good to go.
The author also says that he built a utility to help do all of this for you:
http://tools.andreloker.de/dbag
Expanding on #stijn answer, instead of using regex it is far easier to use native xml parsing.
Here is my proposed file, it also supports the ability to customize which files are evaluated by using a regex on the filename only.
param([String]$Path, [string]$IncludeMatch, [switch]$AllowPreserve)
Function Test-BadContentExists
{
param (
[parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
[string[]]$Path,
[string]$IncludeMatch,
[switch]$AllowPreserve
)
[xml]$proj = Get-Content -Path $Path
$ContentNodes = ($proj | Select-Xml "//Content|//n:Content" -Namespace #{n='http://schemas.microsoft.com/developer/msbuild/2003'}).Node
if (![string]::IsNullOrEmpty($IncludeMatch)) {
$ContentNodes = $ContentNodes | Where-Object -Property Include -Match $IncludeMatch
}
#remove the always nodes
$ContentNodes = $ContentNodes | Where-Object -Property CopyToOutputDirectory -ne 'Always'
#optionally remove the preserve nodes
if ($AllowPreserve) {
$ContentNodes = $ContentNodes | Where-Object -Property CopyToOutputDirectory -ne 'PreserveNewest'
}
if($ContentNodes)
{
write-output "Found file with bad content node:"
write-output ($ContentNodes | Select-Object Include,CopyToOutputDirectory | sort Include | Out-String)
exit 1
}
}
[hashtable]$Options = $PSBoundParameters
[void]$Options.Remove("Path")
Get-ChildItem -Recurse -Include *.csproj -Path $Path | Test-BadContentExists #Options
and calling it, with parameter:
<Target Name="FindBadContentNodes">
<Exec Command="Powershell FindBadContentNodes.ps1 -inputDir path\to\sourceDir -IncludeMatch '^Upgrade.*\.(sql|xml)$'"/>
</Target>
I ended up using a pre-build event instead and put this ps1 file in my solution directory so i could use it with multiple projects.
echo "Build Dir: %cd%"
echo "Sol Dir: $(SolutionDir)"
echo "Proj Dir: '$(ProjectDir)"
echo.
Powershell -NoProfile -Command "& '$(SolutionDir)\FindBadContentNodes.ps1' -Path '$(ProjectDir)' -IncludeMatch '^Upgrade.*\.(sql|xml)$'"
example build output:
1> "Build Dir: C:\Source\RPS\MRM BI\MRMBI-Setup\MRMBI-Schema\bin\Debug"
1> "Sol Dir: C:\Source\RPS\MRM BI\MRMBI-Setup\"
1> "Proj Dir: 'C:\Source\RPS\MRM BI\MRMBI-Setup\MRMBI-Schema\"
1>
1> Found file with bad content node:
1>
1> Include CopyToOutputDirectory
1> ------- ----------------------
1> Upgrades\V17.09\myfile1.sql
1> Upgrades\V20.05\myfile2.sql PreserveNewest
1>

How to fully clean bin and obj folders within Visual Studio?

If you right click on a folder, you will see a "Clean" menu item. I assumed this would clean (remove) the obj and bin directory.
However, as far as I can see, it does nothing.
Is there another way?
(please don't tell me to go to Windows Explorer or the cmd.exe)
I'd like to remove the obj and bin folder so that I can easily zip the whole thing.
As others have responded already Clean will remove all artifacts that are generated by the build. But it will leave behind everything else.
If you have some customizations in your MSBuild project this could spell trouble and leave behind stuff you would think it should have deleted.
You can circumvent this problem with a simple change to your .*proj by adding this somewhere near the end :
<Target Name="SpicNSpan"
AfterTargets="Clean">
<RemoveDir Directories="$(OUTDIR)"/>
</Target>
Which will remove everything in your bin folder of the current platform/configuration.
------ Edit
Slight evolution based on Shaman's answer below (share the votes and give him some too)
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
---- Edit again with parts from xDisruptor but I removed the .vs deletion as this would be better served in a .gitignore (or equivalent)
Updated for VS 2015.
<Target Name="SpicNSpan" AfterTargets="Clean"> <!-- common vars https://msdn.microsoft.com/en-us/library/c02as0cs.aspx?f=255&MSPPError=-2147217396 -->
<RemoveDir Directories="$(TargetDir)" /> <!-- bin -->
<RemoveDir Directories="$(ProjectDir)$(BaseIntermediateOutputPath)" /> <!-- obj -->
</Target>
He also provides a good suggestion on making the task easier to deploy and maintain if you have multiple projects to push this into.
If you vote this answer be sure to vote them both as well.
If you are using git and have a correct .gitignore in your project, you can
git clean -xdf --dry-run
to remove absolutely every file on the .gitignore list, i.e. it will clean obj, and bin folders (the x triggers this behavior)
Note: The parameter --dry-run will only simulate the operation ("Would remove ...") and show you what git would delete. Try it with dry-run, then remove the parameter and it will really delete the files+folders.
Optionally, after that clean command, you can use dotnet restore mySolution.sln to get all the NUGET packages restored. And if you have a developer console open anyway, you can quickly run msbuild -m mySolution.sln afterwards (without having Visual Studio open) to see if it was successful.
For Visual Studio 2015 the MSBuild variables have changed a bit:
<Target Name="SpicNSpan" AfterTargets="Clean"> <!-- common vars https://msdn.microsoft.com/en-us/library/c02as0cs.aspx?f=255&MSPPError=-2147217396 -->
<RemoveDir Directories="$(TargetDir)" /> <!-- bin -->
<RemoveDir Directories="$(SolutionDir).vs" /> <!-- .vs -->
<RemoveDir Directories="$(ProjectDir)$(BaseIntermediateOutputPath)" /> <!-- obj -->
</Target>
Notice that this snippet also wipes out the .vs folder from the root directory of your solution. You may want to comment out the associated line if you feel that removing the .vs folder is an overkill. I have it enabled because I noticed that in some third party projects it causes issues when files ala application.config exist inside the .vs folder.
Addendum:
If you are into optimizing the maintainability of your solutions you might want to take things one step further and place the above snippet into a separate file like so:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="SpicNSpan" AfterTargets="Clean"> <!-- common vars https://msdn.microsoft.com/en-us/library/c02as0cs.aspx?f=255&MSPPError=-2147217396 -->
<RemoveDir Directories="$(TargetDir)" /> <!-- bin -->
<RemoveDir Directories="$(SolutionDir).vs" /> <!-- .vs -->
<RemoveDir Directories="$(ProjectDir)$(BaseIntermediateOutputPath)" /> <!-- obj -->
</Target>
</Project>
And then include this file at the very end of each and every one of your *.csproj files like so:
[...]
<Import Project="..\..\Tools\ExtraCleanup.targets"/>
</Project>
This way you can enrich or fine-tune your extra-cleanup-logic centrally, in one place without going through the pains of manually editing each and every *.csproj file by hand every time you want to make an improvement.
To delete bin and obj before build add to project file:
<Target Name="BeforeBuild">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
Here is article: How to remove bin and/or obj folder before the build or deploy
This site: https://sachabarbs.wordpress.com/2014/10/24/powershell-to-clean-visual-studio-binobj-folders/ uses William Kempf's powershell commands to remove any bin and obj folders from the current directory and sub directories. It should be possible to run it from the root of the drive.
Here is William's version
gci -inc bin,obj -rec | rm -rec -force
In William's own words:
That wipes out all of the “bin” and “obj” directories in the current
directory and every subdirectory. Super useful to run in your
workspace directory to get to a “clean” state, especially when someone
messes up and there’s something that a Clean or Rebuild inside the IDE
doesn’t catch.
For those of you reading that may not know, PowerShell supports
command aliases, here it is rewritten again not using the aliases
Get-ChildItem -inc bin,obj -rec | Remove-Item -rec -force
NOTE : You should have this stored in a PowerShell file and place that
file at the root of your solution (where the .sln file resides), and
then run it when you want a proper clean (not the micky mouse one that
VisualStudio does, and reports success too).
Check out Ron Jacobs fantastic open source CleanProject It even takes care of the zipping if you like.
Here is the CodePlex link
Visual Studio Extension
Right Click Solution - Select "Delete bin and obj folders"
You can easily find and remove bin and obj folders in Far Manager.
Navigate to you solution and press Alt+F7
In search setting dialog:
Type "bin,obj" in field "A file mask or several file masks"
Check option "Search for folders"
Press Enter
After the search is done, switch view to "Panel".
Select all files (with Ctrl+A) and delete folders (press "Shift+Del")
Hope it helps someone.
Based on Joe answer, I've converted the VB code into C# :
/// <summary>
/// Based on code of VSProjCleaner tool (C) 2005 Francesco Balena, Code Archirects
/// </summary>
static class VisualStudioCleaner
{
public static void Process(string rootDir)
{
// Read all the folder names in the specified directory tree
string[] dirNames = Directory.GetDirectories(rootDir, "*.*", SearchOption.AllDirectories);
List<string> errorsList = new List<string>();
// delete any .suo and csproj.user file
foreach (string dir in dirNames) {
var files = new List<string>();
files.AddRange(Directory.GetFiles(dir, "*.suo"));
files.AddRange(Directory.GetFiles(dir, "*.user"));
foreach (string fileName in files) {
try {
Console.Write("Deleting {0} ...", fileName);
File.Delete(fileName);
Console.WriteLine("DONE");
} catch (Exception ex) {
Console.WriteLine();
Console.WriteLine(" ERROR: {0}", ex.Message);
errorsList.Add(fileName + ": " + ex.Message);
}
}
}
// Delete all the BIN and OBJ subdirectories
foreach (string dir in dirNames) {
string dirName = Path.GetFileName(dir).ToLower();
if (dirName == "bin" || dirName == "obj") {
try {
Console.Write("Deleting {0} ...", dir);
Directory.Delete(dir, true);
Console.WriteLine("DONE");
} catch (Exception ex) {
Console.WriteLine();
Console.WriteLine(" ERROR: {0}", ex.Message);
errorsList.Add(dir + ": " + ex.Message);
}
}
}
Console.WriteLine(new string('-', 60));
if (errorsList.Count == 0) {
Console.WriteLine("All directories and files were removed successfully");
} else {
Console.WriteLine("{0} directories or directories couldn't be removed", errorsList.Count);
Console.WriteLine(new string('-', 60));
foreach (string msg in errorsList) {
Console.WriteLine(msg);
}
}
}
}
In windows just open the explorer
navigate to your SLN folder
click into search field and type kind:=folder;obj --> for obj folders
use CTRL+A and delete 'em - same for bin
Done
No need for any tool or extra software ;)
Clean will remove all intermediate and final files created by the build process, such as .obj files and .exe or .dll files.
It does not, however, remove the directories where those files get built.
I don't see a compelling reason why you need the directories to be removed.
Can you explain further?
If you look inside these directories before and after a "Clean", you should see your compiled output get cleaned up.
I use VisualStudioClean which is easy to understand and predictable. Knowing how it works and what files it is going to delete relieves me.
Previously I tried VSClean (note VisualStudioClean is not VSClean), VSClean is more advanced, it has many configurations that sometimes makes me wondering what files it is going to delete? One mis-configuration will result in lose of my source codes. Testing how the configuration will work need backing up all my projects which take a lot of times, so in the end I choose VisualStudioClean instead.
Conclusion : VisualStudioClean if you want basic cleaning, VSClean for more complex scenario.
I can't add a comment yet (no minimal reputation reached)
so I leave this reply to underline that:
the "BeforeBuild" action with <RemoveDir Directories="$(BaseIntermediateOutputPath)" /> is great but, for me, is conflicting with an Entity Framework model included into the same project.
The error I receive is:
Error reading resource '{mymodel}.csdl' -- 'Could not find a part of the path '{myprojectpath}\obj\Release\edmxResourcesToEmbed\{mymodel}.csdl
I suppose, the "edmxResourcesToembed" is created before the "BeforeBuild" target action is executed.
This is how I do with a batch file to delete all BIN and OBJ folders recursively.
Create an empty file and name it DeleteBinObjFolders.bat
Copy-paste code the below code into the DeleteBinObjFolders.bat
Move the DeleteBinObjFolders.bat file in the same folder with your solution (*.sln) file.
#echo off
#echo Deleting all BIN and OBJ folders...
for /d /r . %%d in (bin,obj) do #if exist "%%d" rd /s/q "%%d"
#echo BIN and OBJ folders successfully deleted :) Close the window.
pause > nul
Update: Visual Studio 2019 (Clean [bin] and [obj] before release). However I am not sure if [obj] needs to be deleted. Be aware there is nuget package configuration placed too. You can remove the second line if you think so.
<Target Name="PreBuild" BeforeTargets="PreBuildEvent" Condition="'$(Configuration)' == 'Release'">
<!--remove bin-->
<Exec Command="rd /s /q "$(ProjectDir)$(BaseOutputPath)" && ^" />
<!--remove obj-->
<Exec Command="rd /s /q "$(BaseIntermediateOutputPath)Release"" />
</Target>
I store my finished VS projects by saving only source code.
I delete BIN, DEBUG, RELEASE, OBJ, ARM and .vs folders from all projects.
This reduces the size of the project considerably. The project
must be rebuilt when pulled out of storage.
Just an addendum to all the fine answers above in case someone doesn't realize how easy it is in VB/C# to automate the entire process down to the zip archive.
So you just grab a simple Forms app from the templates (if you don't already have a housekeeping app) and add a button to it and then ClickOnce install it to your desktop without worrying about special settings or much of anything. This is all the code you need to attach to the button:
Imports System.IO.Compression
Private Sub btnArchive_Click(sender As Object, e As EventArgs) Handles btnArchive.Click
Dim src As String = "C:\Project"
Dim dest As String = Path.Combine("D:\Archive", "Stub" & Now.ToString("yyyyMMddHHmmss") & ".zip")
If IsProjectOpen() Then 'You don't want Visual Studio holding a lock on anything while you're deleting folders
MsgBox("Close projects first, (expletive deleted)", vbOKOnly)
Exit Sub
End If
If MsgBox("Are you sure you want to delete bin and obj folders?", vbOKCancel) = DialogResult.Cancel Then Exit Sub
If ClearBinAndObj(src) Then ZipFile.CreateFromDirectory(src, dest)
End Sub
Public Function ClearBinAndObj(targetDir As String) As Boolean
Dim dirstodelete As New List(Of String)
For Each d As String In My.Computer.FileSystem.GetDirectories(targetDir, FileIO.SearchOption.SearchAllSubDirectories, "bin")
dirstodelete.Add(d)
Next
For Each d As String In My.Computer.FileSystem.GetDirectories(targetDir, FileIO.SearchOption.SearchAllSubDirectories, "obj")
dirstodelete.Add(d)
Next
For Each d In dirstodelete
Try
Directory.Delete(d, True)
Catch ex As Exception
If MsgBox("Error: " & ex.Message & " - OK to continue?", vbOKCancel) = MsgBoxResult.Cancel Then Return False
End Try
Next
Return True
End Function
Public Function IsProjectOpen()
For Each clsProcess As Process In Process.GetProcesses()
If clsProcess.ProcessName.Equals("devenv") Then Return True
Next
Return False
End Function
One thing to remember is that file system deletes can go wrong easily. One of my favorites was when I realized that I couldn't delete a folder because it contained items created by Visual Studio while running with elevated privileges (so that I could debug a service).
I needed to manually give permission or, I suppose, run the app with elevated privileges also. Either way, I think there is some value in using an interactive GUI-based approach over a script, specially since this is likely something that is done at the end of a long day and you don't want to find out later that your backup doesn't actually exist...
this answer is great I just want to comment on the last part of the answer
NOTE : You should have this stored in a PowerShell file and place that
file at the root of your solution (where the .sln file resides), and
then run it when you want a proper clean (not the micky mouse one that
VisualStudio does, and reports success too).
Alternatively, you can add the following to your profile.ps1
function CleanSolution {
Get-ChildItem -inc bin,obj -rec | Remove-Item -rec -force
}
Set-Alias cs CleanSolution
Then you can use either CleanSolution or cs to run. That way you can use it for any project and without the ./ prefix of the filename
Complete one-liner you can invoke from within Visual Studio
In your solution root folder create a file called "CleanBin.bat" and add the following one-liner:
Powershell.exe -ExecutionPolicy Bypass -NoExit -Command "Get-ChildItem -inc bin,obj -rec | Remove-Item -rec -force"
Run the .bat file. Enjoy.
Original creds to the answer here: https://stackoverflow.com/a/43267730/1402498
The original answer shows the powershell command, but I had a lot of trouble making it work smoothly on my system. I finally arrived at the one-liner above, which should work nicely for most folks.
Caveat:
Microsoft seems to be great at making Windows security cause stranger and stranger behavior. On my machine, when I run the script, all obj and bin folders are deleted but then reappear 2 seconds later! Running the script a second time causes permanent deletion. If anyone knows what would cause this behavior, please let me know a fix and I'll update the answer.
for visual studio 2022
you can use:
https://marketplace.visualstudio.com/items?itemName=MadsKristensen.CleanSolution
If you need to delete bin and obj folders from ALL of your projects...
Launch git Bash and enter the following command:
find . -iname "bin" -o -iname "obj" | xargs rm -rf
For C# projects, I recommend appending $(Configuration) to obj folder, so-as to avoid deleting nuget files which are stored on obj base directory.
<Target Name="CleanAndDelete" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)$(Configuration)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
If you delete the nuget files, it can be problematic to recreate them. Moreover, I've never seen a case where "Restore NuGet Packages" fixes this issue after these files have been deleted.

Resources