Skip to content

Instantly share code, notes, and snippets.

@lukasjuhrich
Last active June 2, 2020 04:59
Show Gist options
  • Save lukasjuhrich/250bc2843f6441ac4a885f118388c1ab to your computer and use it in GitHub Desktop.
Save lukasjuhrich/250bc2843f6441ac4a885f118388c1ab to your computer and use it in GitHub Desktop.
SDK-Style migration of `.csproj` files
import fileinput as fi
import logging
import re
import sys
import typing
logging.basicConfig(
#filename='migrator.log',
level=logging.INFO,
format= '[%(levelname)s] - %(message)s',
)
logger = logging.getLogger('migrator')
def main(args: typing.List[str]):
for filename in args:
migrate_file(filename)
def pre_whitespace(line: str) -> str:
return re.match(r'^(\s*)', line).group(1)
def migrate_file(file: str):
logger.info("Migrating file %s", file)
with open(file) as f:
found = False
for i, line in enumerate(f):
if i > 5:
break
if 'Sdk=' in line:
found = True
break
if found:
logger.warning("File %s already migrated. Aborting!", file)
return
with fi.input(file, backup='.bak', inplace=True) as f:
for line in f:
ws = pre_whitespace(line)
# Remove XML line
if any(['<?xml' in line,
is_superfluous_import(line),
is_cs_definition(line),
is_resx_oneline_include(line),
is_superfluous_header_line(line)]):
logger.debug("Skipping line %d b/c of preconditions.", f.lineno())
continue
if is_cs_multiline_include(line) or is_resx_multiline_include(line):
logger.info("Replacing Multiline include on line %d", f.lineno())
new_line = line.replace('Include=', 'Update=')
#logger.info(new_line)
print(new_line, end='')
continue
if '<Project ' in line:
logger.info("Replacing '<Project>' line")
print(ws + '<Project Sdk="Microsoft.NET.Sdk">')
continue
if 'TargetFrameworkVersion' in line:
fw = extract_target_framework(line)
logger.info("Framework %s", fw)
print(ws + f'<TargetFramework>{fw}</TargetFramework>')
continue
if re.match(r'^.*<NuGetPackageImportStamp>\s*$', line):
logger.info("Removing Nuget import stamp on line %d", f.lineno())
_next_line = f.readline()
if '/nugetpackageimportstamp' not in _next_line.lower():
logger.warning("Expected </NuGetPackageImportStamp> on %s line %d", file, f.lineno())
continue
if '<PackageReference' in line:
logger.info("Migrating <PackageReference> on line %d", f.lineno())
print(ws + new_pkgref_line(pkgref_line=line, version_line=f.readline()))
_endtag_line = f.readline()
if '/PackageReference' not in _endtag_line:
logger.warning("Expected </PackageReference> on %s line %d", file, f.lineno())
continue
if '<ProjectReference' in line:
logger.info("Migrating <ProjectReference> on line %d", f.lineno())
_guid_line = f.readline()
if '<Project>' not in _guid_line:
logger.warning("Expected <Project>-GUID-Tag on %s line %d", file, f.lineno())
_name_line = f.readline()
if '<Name>' not in _name_line:
logger.warning("Expected <Name>-Tag on %s line %d", file, f.lineno())
print(ws + new_projref_line(projref_line=line))
_endtag_line = f.readline()
if '/ProjectReference' not in _endtag_line:
logger.warning("Expected </ProjectReference on %s line %d", file, f.lineno())
continue
# logger.info("Plain-printing line %d", f.lineno())
print(line, end='')
def is_superfluous_header_line(line: str) -> bool:
fragments = [
"<Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>",
"<Platform Condition=\" '$(Platform)' == '' \">AnyCPU</Platform>",
'<OutputType>Library</OutputType>',
'<AppDesignerFolder>Properties</AppDesignerFolder>',
# '<RootNamespace />ClassLibrary1</RootNamespace>', # I think that's NOT superfluous!
# '<AssemblyName>ClassLibrary1</AssemblyName>',
'<Optimize>false</Optimize>',
'<ProjectGuid>',
'<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>',
'<FileAlignment>512</FileAlignment>',
'<DebugSymbols>true</DebugSymbols>',
'<DebugType>full</DebugType>',
# r'<OutputPath>bin\Debug\</OutputPath>', # This is wrong, appearently
'<DefineConstants>DEBUG;TRACE</DefineConstants>',
'<DefineConstants>TRACE</DefineConstants>',
'<ErrorReport>prompt</ErrorReport>',
'<WarningLevel>4</WarningLevel>',
'<NuGetPackageImportStamp></NuGetPackageImportStamp>',
'<TargetFrameworkProfile />',
'<TestProjectType>UnitTest</TestProjectType>',
'<IsCodedUITest>False</IsCodedUITest>',
r'<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>',
"<VSToolsPath Condition=\"'$(VSToolsPath)' == ''\">$(MSBuildExtensionsPath32)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion)</VSToolsPath>",
"<VisualStudioVersion Condition=\"'$(VisualStudioVersion)' == ''\">10.0</VisualStudioVersion>",
]
return any(
fragment.lower() in line.lower() for fragment in fragments
)
def extract_target_framework(line: str) -> str:
match = re.search(r'>v([0-9.]+)<', line)
version_numbers = match.group(1).split('.')
return "net" + ''.join(version_numbers)
def new_pkgref_line(pkgref_line: str, version_line: str) -> str:
pkgref = re.search(r'<PackageReference Include="([^"]+)">', pkgref_line.strip()).group(1)
version = re.search(r'<Version>(.*)</Version>', version_line.strip()).group(1)
return f'<PackageReference Include="{pkgref}" Version="{version}" />'
def new_projref_line(projref_line: str) -> str:
projref = re.search(r'<ProjectReference Include="([^"]+)">', projref_line.strip()).group(1)
return f'<ProjectReference Include="{projref}" />'
def is_superfluous_import(line: str) -> bool:
return '<Import ' in line and any([
r'\Microsoft.Common.props"' in line,
r'\Microsoft.CSharp.targets"' in line,
r'\Microsoft.TestTools.targets"' in line,
])
def is_cs_definition(line: str) -> bool:
return bool(re.match(r'^\s*<Compile.*Include="[^"]+\.cs"\s*/>\s*$', line))
def is_cs_multiline_include(line: str) -> bool:
if r'Include="..' in line:
return False
return bool(re.match(r'^\s*<Compile.*Include="[^"]+\.cs"\s*>\s*$', line))
def is_resx_oneline_include(line: str) -> bool:
return bool(re.match(r'^\s*<EmbeddedResource.*Include="[^"]+\.resx"\s*/>\s*$', line))
def is_resx_multiline_include(line: str) -> bool:
return bool(re.match(r'^\s*<EmbeddedResource.*Include="[^"]+\.resx"\s*>\s*$', line))
def test_framework_version():
f = extract_target_framework
assert f("sion>v4.7.2<Tar") == 'net472'
def test_pkgref_line():
f = new_pkgref_line
assert f(
' <PackageReference Include="EntityFramework">',
' <Version>6.4.0</Version>',
) == '<PackageReference Include="EntityFramework" Version="6.4.0" />'
assert f(
' <PackageReference Include="Foo.Bar.Baz"> ',
' <Version>2.12.1</Version> ',
) == '<PackageReference Include="Foo.Bar.Baz" Version="2.12.1" />'
assert f(
' <PackageReference Include="JetBrains.Annotations">',
' <Version>2019.3.1</Version>',
) == '<PackageReference Include="JetBrains.Annotations" Version="2019.3.1" />'
def test_projref_line():
f = new_projref_line
assert f(
r' <ProjectReference Include="..\..\Data\Sql.Generic\Data.Sql.Generic.csproj"> ',
) == r'<ProjectReference Include="..\..\Data\Sql.Generic\Data.Sql.Generic.csproj" />'
def test_cs_line():
f = is_cs_definition
assert f(r' <Compile Include="Foo\Bar\Baz.cs" />')
assert f(r' <Compile Include="Foo\Bar\Baz.cs"/> ')
assert not f(r'<Compile Update="Foo\Bar\Baz.cs" />')
assert not f(r'<Compile Include="Foo\Bar\Baz.notcs" />')
if __name__ == '__main__':
main([a for a in sys.argv if a.endswith('.csproj')])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment