Last active
June 2, 2020 04:59
-
-
Save lukasjuhrich/250bc2843f6441ac4a885f118388c1ab to your computer and use it in GitHub Desktop.
SDK-Style migration of `.csproj` files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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