[NoBrainer] PowerShell modules, digital signatures, nuspec files and packages automated

As announced a couple of days ago we started pushing some of our PowerShell modules to GitHub and NuGet. And soon it became obvious that keeping all the scripts, signatures, versions and packages in sync was some tedious and error-prone work. This should definitely be automated, you might instantly think – and right you are! So here are some small scripts let that you take care of some of the repetitive tasks…

Prerequisites

  • a base folder with all your script modules
  • a digital certificate for code signing
    in case you do not have a certificate and if you are in Switzerland, you can cheaply get a code signing certificate by buying a Post SuisseID which also serves that purpose.
  • PowerShell modules with manifest files
  • a nuspec template file

Update-Signature

Update-Signature lets you easily sign all your script files (ps1, psm1, psd1) recursively. It makes use of the Microsoft supplied Cmdlet Set-AuthenticodeSignature which applies a authenticode signature comment block at the end of the scripts.

Update-Signature C:\github
Update-Signature C:\github\myscript.ps1

The script will process all scripts if a folder is specified but also accepts pipeline input for individual files.

Update-ManifestRevision

Update-ManifestRevision compare the version and revision of the manifest file of a PowerShell module against the last write time of the files in the module folder. If the last write time is newer the revision of the module manifest is automatically adjusted and the signature is updated as well.

# Version number of this module.
ModuleVersion = '1.0.4.20141130'

Again, you can either scan a folder path recursively or specify individual files.

Update-Nuspec

Update-Nuspec lets you create a new nuspec file (and a nupkg) if the version number in the module manifest is higher than the highest existing version number of a nuspec file in the module folder. When the nuspec file is created all referencesd items from the manifest are included in the nuspec file (such as RootModule, NestedModules, FileList, and the manifest itself).

Update-Nuspec C:\github\biz.dfch.PS.module\biz.dfch.PS.module.psd1

If the module vesion of biz.dfch.PS.module.psd1 is ‘1.0.4.20141130′ then this will create a new nuspec file: ‘C:\github\biz.dfch.PS.module\biz.dfch.PS.module.1.0.4.nuspec‘.

Once more, you can either scan a folder path recursively or specify individual files.

Summary

With these three easy steps you can automate most of your PowerShell post-build tasks. You can find the scripts in our Gist at https://gist.github.com/dfch/5a263d63e1967b7e1892

PowerShell modules, digital signatures, NuGet nuspec and packages

d-fens GmbH
General-Guisan-Strasse 6
CH-6300 Zug
Switzerland

d-fens PowerShell modules, digital signatures, NuGet nuspec and packages
Copyright 2014 d-fens GmbH
This product includes software developed at
d-fens GmbH (https://d-fens.ch/).

view raw
NOTICE
hosted with ❤ by GitHub

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>biz.dfch.PS.System.Utilities</id>
<version>1.0.4</version>
<title>biz.dfch.PS.System.Utilities</title>
<authors>d-fens GmbH</authors>
<owners>Ronald Rink</owners>
<licenseUrl>https://github.com/dfch/biz.dfch.PS.System.Utilities/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/dfch/biz.dfch.PS.System.Utilities</projectUrl>
<iconUrl>https://raw.githubusercontent.com/dfch/fragments/master/logo-32×32.png</iconUrl>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<description>biz.dfch.PS.System.Utilities
============================
Modules: biz.dfch.PS.System.Utilities
d-fens GmbH, General-Guisan-Strasse 6, CH-6300 Zug, Switzerland
This Microsoft PowerShell module contains Cmdlets to perform various actions and utilties/convenience functions such as string conversion and formatting.
You can download this module via [NuGet](http://nuget.org) with [Install-Package biz.dfch.PS.System.Utilities](https://www.nuget.org/packages/biz.dfch.PS.System.Utilities/).
See [d-fens WebSite](https://d-fens.ch/2014/10/15/module-biz-dfch-ps-system-utilities/) or check the [Wiki](https://github.com/dfch/biz.dfch.PS.System.Utilities/wiki) for further description and examples on how to use the module.
</description>
<summary>This Microsoft PowerShell module contains Cmdlets to perform various actions and utilties/convenience functions such as string conversion and formatting.</summary>
<releaseNotes>20141130</releaseNotes>
<copyright>(c) 2014 d-fens GmbH</copyright>
<language>en-US</language>
<tags>PowerShell dfch</tags>
<dependencies>
<dependency id="biz.dfch.PS.System.Logging" version="1.0.3" />
</dependencies>
</metadata>
<files xmlns="">
<file src="biz.dfch.PS.System.Utilities.psm1" target="biz.dfch.PS.System.Utilities.psm1" />
<file src="biz.dfch.PS.System.Utilities.psd1" target="biz.dfch.PS.System.Utilities.psd1" />
<file src="biz.dfch.PS.System.Utilities.xml" target="biz.dfch.PS.System.Utilities.xml" />
<file src="LICENSE" target="LICENSE" />
<file src="NOTICE" target="NOTICE" />
<file src="README.md" target="README.md" />
<file src="biz.dfch.PS.System.Utilities_aaab9f3e-e544-4827-9db8-44bade441fc5_en-US_HelpContent.cab" target="biz.dfch.PS.System.Utilities_aaab9f3e-e544-4827-9db8-44bade441fc5_en-US_HelpContent.cab" />
<file src="biz.dfch.PS.System.Utilities_aaab9f3e-e544-4827-9db8-44bade441fc5_HelpInfo.xml" target="biz.dfch.PS.System.Utilities_aaab9f3e-e544-4827-9db8-44bade441fc5_HelpInfo.xml" />
<file src="New-CustomErrorRecord.ps1" target="New-CustomErrorRecord.ps1" />
<file src="Format-Xml.ps1" target="Format-Xml.ps1" />
<file src="ConvertFrom-UnicodeHexEncoding.ps1" target="ConvertFrom-UnicodeHexEncoding.ps1" />
<file src="ConvertFrom-SecureStringDF.ps1" target="ConvertFrom-SecureStringDF.ps1" />
<file src="New-SecurePassword.ps1" target="New-SecurePassword.ps1" />
<file src="ConvertTo-UrlEncoded.ps1" target="ConvertTo-UrlEncoded.ps1" />
<file src="ConvertFrom-UrlEncoded.ps1" target="ConvertFrom-UrlEncoded.ps1" />
<file src="ConvertTo-Base64.ps1" target="ConvertTo-Base64.ps1" />
<file src="ConvertFrom-Base64.ps1" target="ConvertFrom-Base64.ps1" />
<file src="Get-ComObjectType.ps1" target="Get-ComObjectType.ps1" />
<file src="Test-StringPattern.ps1" target="Test-StringPattern.ps1" />
<file src="Import-Credential.ps1" target="Import-Credential.ps1" />
<file src="Export-Credential.ps1" target="Export-Credential.ps1" />
<file src="Get-Constructor.ps1" target="Get-Constructor.ps1" />
<file src="Set-SslSecurityPolicy.ps1" target="Set-SslSecurityPolicy.ps1" />
<file src="New-ActivityProgress.ps1" target="New-ActivityProgress.ps1" />
<file src="Set-ActivityProgress.ps1" target="Set-ActivityProgress.ps1" />
<file src="Remove-ActivityProgress.ps1" target="Remove-ActivityProgress.ps1" />
<file src="ConvertFrom-CmdletHelp.ps1" target="ConvertFrom-CmdletHelp.ps1" />
<file src="Expand-CompressedItem.ps1" target="Expand-CompressedItem.ps1" />
<file src="Format-IpAddress.ps1" target="Format-IpAddress.ps1" />
<file src="ConvertFrom-PSCustomObject.ps1" target="ConvertFrom-PSCustomObject.ps1" />
<file src="ConvertFrom-Hashtable.ps1" target="ConvertFrom-Hashtable.ps1" />
<file src="Test-CmdletDocumentation.ps1" target="Test-CmdletDocumentation.ps1" />
<file src="Assert-CmdletDocumentation.ps1" target="Assert-CmdletDocumentation.ps1" />
</files>
</package>

function Update-Manifest {
<#
.SYNOPSIS
Updates the revision number of a PowerShell manifest.
.DESCRIPTION
Updates the revision number of a PowerShell manifest.
The revision of the specified manifest is checked against the files in the module folder. If the revision is older than the last write time the revision is updated.
.EXAMPLE
Update-Signature C:\scripts\myScript.ps1
Checks the specified manifest.
.EXAMPLE
Update-Signature C:\scripts
Checks all manifest in the specified folder recursively.
#>
[CmdletBinding(
SupportsShouldProcess = $true
,
ConfirmImpact = "Low"
,
DefaultParameterSetName = 'path'
)]
PARAM
(
# Specifies a path to search for manifest files
[ValidateScript( { Test-Path($_) PathType Container; } )]
[Parameter(Mandatory = $false, ParameterSetName = 'path')]
[System.IO.DirectoryInfo] $Path = 'C:\Github'
,
# Specifies one or more manifest files to check
[ValidateScript( { if($_) { foreach($item in $_) {Test-Path($item) PathType Leaf Include "*.psd1"; } } else { $true } } )]
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'file')]
[AllowNull()]
[Alias('File')]
$InputObject
,
[Parameter(Mandatory = $false)]
[switch] $IncrementVersionMajor = $false
,
[Parameter(Mandatory = $false)]
[switch] $IncrementVersionMinor = $false
,
[Parameter(Mandatory = $false)]
[switch] $IncrementVersionBuild = $false
,
[Parameter(Mandatory = $false)]
[switch] $UpdateRevision = $true
)
BEGIN
{
if($PSCmdlet.ParameterSetName -eq 'path')
{
$InputObjectTemp = (Get-ChildItem $Path Include "*.psd1" Recurse);
if($InputObjectTemp)
{
$InputObject = $InputObjectTemp;
Remove-Variable InputObjectTemp;
}
}
}
# BEGIN
PROCESS
{
foreach($Object in $InputObject)
{
try
{
$Version = (Get-Content Raw $Object | iex).ModuleVersion -as [Version];
}
catch
{
$Version = (Get-Content Raw $Object | iex).ModuleVersion -as [Version];
}
$VersionNew = $Version;
$Object = Get-Item $Object;
if($UpdateRevision)
{
Write-Host ("{0}: Comparing against Version.Revision '{1}'" -f $Object.FullName, $Version.Revision);
$LastWriteTimes = (Get-ChildItem (Split-Path $Object)).LastWriteTime;
foreach($LastWriteTime in $LastWriteTimes)
{
if( ($LastWriteTime.ToString('yyyyMMdd') -as [int]) -gt $Version.Revision)
{
$VersionNew = New-Object Version($VersionNew.Major, $VersionNew.Minor, $VersionNew.Build, ($LastWriteTime.ToString('yyyyMMdd') -as [int]));
}
}
}
if($IncrementVersionMajor)
{
Write-Host ("{0}: Incrementing Version.Major '{1}'" -f $Object.FullName, $Version.Major);
$VersionNew = New-Object Version(($VersionNew.Major +1), $VersionNew.Minor, $VersionNew.Build, $VersionNew.Revision);
}
if($IncrementVersionMinor)
{
Write-Host ("{0}: Incrementing Version.Minor '{1}'" -f $Object.FullName, $Version.Minor);
$VersionNew = New-Object Version($VersionNew.Major, ($VersionNew.Minor +1), $VersionNew.Build, $VersionNew.Revision);
}
if($IncrementVersionBuild)
{
Write-Host ("{0}: Incrementing Version.Build '{1}'" -f $Object.FullName, $Version.Build);
$VersionNew = New-Object Version($VersionNew.Major, $VersionNew.Minor, ($VersionNew.Build +1), $VersionNew.Revision);
}
if($Version.ToString() -ne $VersionNew.ToString())
{
if(!$PSCmdlet.ShouldProcess($Object))
{
continue;
}
Write-Host ("{0}: Updating manifest revision from '{1}' to '{2}' …" -f $Object, $Version.ToString(), $VersionNew.ToString());
(Get-Content Raw $Object).Replace($Version.ToString(), $VersionNew.ToString()) | Out-File Encoding default $Object Confirm:$false;
$null = Update-Signature $Object.FullName Confirm:$false;
$Object.FullName;
}
}
}
# PROCESS
END
{
}
# END
} # function
if($MyInvocation.ScriptName -And ('.' -ne $MyInvocation.InvocationName)) { Export-ModuleMember Function Update-Manifest; }
#
# Copyright 2014-2015 Ronald Rink, d-fens GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

view raw
Update-Manifest.ps1
hosted with ❤ by GitHub

function Update-Nuspec {
<#
.SYNOPSIS
Updates the nuspec file of a PowerShell module.
.DESCRIPTION
Updates the nuspec file of a PowerShell module.
Checks the version and revision number of a PowerShell module manifest and
create a new nuspec file if necessary. The new nuspec file will be built from
a previous existing nuspec (with the highest available version number).
All files referenced in the manifest will be inserted into the nusepc file.
Optionally a nupkg will be created as well.
.EXAMPLE
Update-Nuspec C:\scripts\biz.dfch.PS.Module\biz.dfch.PS.Module.psd1
Updates the given module manifest with a new nuspec file and also creates a package from it.
.EXAMPLE
Update-Nuspec C:\scripts
Updates all module manifests in the specified path with a new nuspec file and also creates a package from it.
#>
[CmdletBinding(
SupportsShouldProcess = $true
,
ConfirmImpact = "Low"
,
DefaultParameterSetName = 'path'
)]
PARAM
(
# Specifies a path to nuspec files to update
[ValidateScript( { Test-Path($_) PathType Container; } )]
[Parameter(Mandatory = $false, ParameterSetName = 'path')]
[System.IO.DirectoryInfo] $Path = 'C:\Github'
,
# Specifies one ore more nuspec files to update
[ValidateScript( { if($_) { foreach($item in $_) {Test-Path($item) PathType Leaf; } } else { $true } } )]
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'file')]
[AllowNull()]
[Alias('File')]
$InputObject
,
# Specify whether to also create a package from the nuspec file
[Parameter(Mandatory = $false)]
[Alias('CreatePackage')]
[switch] $Pack = $true
,
# Specify whether to update an existing package
[Parameter(Mandatory = $false)]
[switch] $Force = $false
,
# Specifies the path to the NuGet.exe
[ValidateScript( { Test-Path($_); } )]
[Parameter(Mandatory = $false)]
[string] $NuGetExe = 'C:\SOFTWARE\NuGet.exe'
)
if($PSCmdlet.ParameterSetName -eq 'path')
{
$InputObjectTemp = (Get-ChildItem $Path Include "*.psd1" Recurse);
if($InputObjectTemp)
{
$InputObject = $InputObjectTemp;
Remove-Variable InputObjectTemp;
}
}
foreach($Object in $InputObject)
{
$Object = Get-Item $Object;
$Manifest = (Get-Content Raw $Object) | iex;
$Version = (($Manifest).ModuleVersion -as [Version]);
$NuspecVersion = '{0}.{1}.{2}' -f $Version.Major, $Version.Minor, $Version.Build;
$Revision = $Version.Revision;
$NuspecPathAndFile = Join-Path Path (Split-Path $Object Parent) ChildPath ('{0}.{1}.nuspec' -f $Object.BaseName, $NuspecVersion);
if(Test-Path $NuspecPathAndFile)
{
[xml] $NuGetXml = Get-Content Raw $NuspecPathAndFile;
if($NuGetXml.package.metadata.releaseNotes -notmatch $Version.Revision)
{
Write-Warning ("{0}: NuGet package already exists, but does not contain correct Revision '{1}'." -f $NuspecPathAndFile, $Revision);
if(!$Force)
{
continue;
}
}
Write-Host ("{0}: NuGet package already exists." -f $NuspecPathAndFile);
if(!$Force)
{
continue;
}
}
$NuspecPrevious = Get-ChildItem Path (Split-Path $Object Parent) Include "*.nuspec" Recurse | sort Property Name, LastWriteTime Descending | Select First 1;
if(!$NuspecPrevious)
{
Write-Warning ("{0}: No previous NuGet package exists. Skipping …" -f $NuspecPathAndFile);
continue;
}
[xml] $NuGetXml = Get-Content Raw $NuspecPrevious;
# set version
$NuGetXml.package.metadata.version = $NuspecVersion;
# set revision
$NuGetXml.package.metadata.releaseNotes = $Revision.ToString();
# set the description
$NuGetXml.package.metadata.description = (Get-Content Raw (Join-Path Path (Split-Path $Object Parent) ChildPath 'README.md')).ToString();
# set the files to be included
try
{
$NuGetXml.package.files.RemoveAll();
$xmlFiles = $NuGetXml.package.SelectSingleNode('files');
}
catch
{
$xmlFiles = $NuGetXml.CreateElement('files');
}
foreach($item in $Manifest.RootModule,$Object.Name)
{
$xmlFile = $NuGetXml.CreateElement('file');
$xmlSrc = $NuGetXml.CreateAttribute('src');
$xmlSrc.Value = $item;
$xmlTarget = $NuGetXml.CreateAttribute('target');
$xmlTarget.Value = $item;
$null = $xmlFile.Attributes.Append($xmlSrc);
$null = $xmlFile.Attributes.Append($xmlTarget);
$null = $xmlFiles.AppendChild($xmlFile);
}
foreach($item in $Manifest.FileList)
{
$xmlFile = $NuGetXml.CreateElement('file');
$xmlSrc = $NuGetXml.CreateAttribute('src');
$xmlSrc.Value = $item;
$xmlTarget = $NuGetXml.CreateAttribute('target');
$xmlTarget.Value = $item;
$null = $xmlFile.Attributes.Append($xmlSrc);
$null = $xmlFile.Attributes.Append($xmlTarget);
$null = $xmlFiles.AppendChild($xmlFile);
}
foreach($item in $Manifest.NestedModules)
{
$xmlFile = $NuGetXml.CreateElement('file');
$xmlSrc = $NuGetXml.CreateAttribute('src');
$xmlSrc.Value = $item;
$xmlTarget = $NuGetXml.CreateAttribute('target');
$xmlTarget.Value = $item;
$null = $xmlFile.Attributes.Append($xmlSrc);
$null = $xmlFile.Attributes.Append($xmlTarget);
$null = $xmlFiles.AppendChild($xmlFile);
}
$null = $NuGetXml.package.AppendChild($xmlFiles);
if(!$PSCmdlet.ShouldProcess($Object))
{
continue;
}
Write-Host ("{0}: Creating NuSpec file …" -f $NuspecPathAndFile);
$NuGetXml.OuterXml | fx | Out-File Encoding default $NuspecPathAndFile;
if($Pack)
{
Push-Location (Split-Path $Object Parent);
Write-Host ("{0}: Creating NuPkg file …" -f $NuspecPathAndFile);
Start-Process $NuGetExe ArgumentList @( ("pack {0}" -f $NuspecPathAndFile),"-NonInteractive",'-Verbosity quiet') NoNewWindow Wait;
Pop-Location;
}
}
} # function
if($MyInvocation.ScriptName -And ('.' -ne $MyInvocation.InvocationName)) { Export-ModuleMember Function Update-Nuspec; }
#
# Copyright 2014-2015 Ronald Rink, d-fens GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

view raw
Update-Nuspec.ps1
hosted with ❤ by GitHub

#Requires -Modules Microsoft.PowerShell.Security
function Update-Signature {
<#
.SYNOPSIS
Updates the digital signature of PowerShell scripts.
.DESCRIPTION
Updates the digital signature of PowerShell scripts.
You can either sign all scripts in a given folder or only individual files.
.EXAMPLE
Update-Signature C:\scripts\myScript.ps1
Signs the specified script with the default certificate and timestamps it.
.EXAMPLE
Update-Signature C:\scripts
Signs the all scripts in the specified folder with the default certificate and timestamps it.
#>
[CmdletBinding(
SupportsShouldProcess = $true
,
ConfirmImpact = "Low"
,
DefaultParameterSetName = 'path'
)]
PARAM
(
# The path to script files to sign
[ValidateScript( { Test-Path($_) PathType Container; } )]
[Parameter(Mandatory = $false, ParameterSetName = 'path')]
[System.IO.DirectoryInfo] $Path = 'C:\Github'
,
# A script file or array of script files to sign
[ValidateScript( { if($_) { foreach($item in $_) {Test-Path($item) PathType Leaf Include $IncludeExtensions; } } else { $true } } )]
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'file')]
[AllowNull()]
[Alias('File')]
$InputObject
,
# The certificate to use for signing the specified scripts
[Parameter(Mandatory = $false, Position = 1)]
$Cert = (Select-Object First 1 InputObject (Get-ChildItem Path cert:\CurrentUser\my CodeSigningCert))
,
# Specify wheter to timestamp (countersign) the script files
[Parameter(Mandatory = $false)]
[Alias('TimeStamp')]
[switch] $CounterSign = $true
,
# The timestamp server url
[Parameter(Mandatory = $false, Position = 2)]
[Uri] $TimestampServer = 'http://timestamp.globalsign.com/scripts/timstamp.dll'
,
# File extensions of the script files to sign
[Parameter(Mandatory = $false)]
$IncludeExtensions = @('*.ps1','*.psm1','*.psd1','*.dll','*.exe')
)
if($PSCmdlet.ParameterSetName -eq 'path')
{
$InputObjectTemp = (Get-ChildItem $Path Include $IncludeExtensions Recurse | Get-AuthenticodeSignature |? { ($_.Status -eq 'HashMismatch') -Or (($_.TimeStamperCertificate -eq $null) -And ($_.Status -eq 'Valid')) }).Path;
if($InputObjectTemp)
{
$InputObject = $InputObjectTemp;
Remove-Variable InputObjectTemp;
}
}
foreach($Object in $InputObject)
{
if(!$PSCmdlet.ShouldProcess($Object))
{
continue;
}
if($CounterSign)
{
Set-AuthenticodeSignature $Object Certificate $cert TimestampServer $TimestampServer Confirm:$false;
}
else
{
Set-AuthenticodeSignature $Object Certificate $cert Confirm:$false;
}
}
} # function
if($MyInvocation.ScriptName -And ('.' -ne $MyInvocation.InvocationName)) { Export-ModuleMember Function Update-Signature; }
#
# Copyright 2014-2015 Ronald Rink, d-fens GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

view raw
Update-Signature.ps1
hosted with ❤ by GitHub

Trackbacks

  1. […] This approach can also be combined with more formal releases such as NuGet packages as I described in PowerShell modules, digital signatures, nuspec files and packages automated. […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: