Hur man använder CMake utan den plågsamma smärtan - Del 2 (2024)

De åsikter och åsikter som uttrycks på denna webbplats är Alex Reinkings och återspeglar inte nödvändigtvis hans arbetsgivares åsikter eller ståndpunkter.

Mån 31 maj 2021

Välkommen tillbaka till del 2 av denna serie! Jag blev väldigt glad över att se det varma mottagandetDel 1kom över/r/cpp. Innan vi sätter igång tänkte jag passa på att förtydliga ett par punkter om den här serien.

Först,den här serien är inte en handledning, åtminstone inte i traditionell mening. Min förhoppning med detta projekt är att visa dighur man resonerarom CMake så att det känns intuitivt. Jag vill att läsarna ska se helheten och utveckla en smak för kvalitetsbyggkod. Ändå kommer det att finnas en del utrymme för att utforska specifika effektiva metoder och peka ut vanliga misstag, ersatta funktioner etc. men allt med ett öga mot förståelseVarför.

För det andra, medan jag klagade över havet av dåliga CMake-resurser, glömde jag att känna igen handfulla bra resurser som har lärt mig bra.Jag har lagt till en lista över dessa resursertill slu*tet avDel 1.

Idag skulle jag vilja prata omvad du kan förvänta digfrån ett CMake-bygge, och några vanliga fallgropar som bryter mot dessa förväntningar. Inte alla CMake-projekt du stöter på kommer att uppfylla dessa kriterier. Jag skulle uppmuntra dig att inleda en vänlig dialog med underhållarna av projekt som inte uppfyller kraven för att se om de kan fixas (och, i öppen källkods anda, försök att öppna en PR!).

Räkna med att vaniljkroppar fungerar

Jag ska göra ett djärvt påstående här: det ska gå att bygganågraCGör projekt med hjälp avnågragenerator med följande sekvens av kommandon, förutsatt att alla dess beroenden är installerade på systemplatser:

#För a enkelkonfiguration generator:$cmmake -S . -B bygga -DCMAKE_BUILD_TYPE=Släpp$cmmake --bygga bygga$cmmake --Installera bygga --prefix /sökväg/till/vart som helst#För a multi-konfiguration generator:$cmmake -S . -B bygga$cmmake --bygga bygga --config Släpp$cmmake --Installera bygga --config Släpp --prefix /sökväg/till/vart som helst

Dessutom, om koden är standardkompatibel och plattformsoberoende, bör denna sekvens fungera mednågrakompilator pånågraoperativ system.

Fallgrop: onödiga flaggor och inställningar

Uppenbarligen, om du bygger ett endast Linux-verktyg som beror på GNU-tillägg, behöver du GCC eller Clang. Tyvärr antar många CMake-byggen för mycket om miljön eller verktygskedjan och injicerar valfria kompilatorspecifika flaggor i sina byggen. Ofta ger de inget sätt att inaktivera dem. Sådana projekt kan i onödan misslyckas på en annan kompilator eller till och med en annan version av samma kompilator som används av författaren.

Det vanligaste exemplet är att lägga till-Felvillkorslöst. Betydelsen av-Väggförändringar mellan kompilatorversioner, så även om den här koden kanske fungerar för dig idag, löper den stor risk att bitrutta:

# DÅLIGT: gör inte det här!target_compile_options(mål PRIVAT -Vägg -Fel)

För ett mer subtilt exempel tillhandahåller både GCC och Clang varningsflaggor för missade användningar av C++11åsidosättanyckelord. På GCC 5.1 och nyare är det-Wsuggest-overrideoch på Clang 10 och nedan är checken uppdelad mellan två flaggor:-Winconsistent-missing-destructor-overrideoch-Winconsistent-missing-override. Att tillhandahålla en endast Clang-flagga till GCC kommer att leda till ett fel, och tillhandahållande av endast GCC-flaggan till Clang ger en varning som kan uppgraderas till ett fel om-Felanges också. Alltså, om du skriver naivt

# DÅLIGT: gör inte det här!target_compile_options(mål PRIVAT -Winconsistent-missing-override)

då kommer ditt bygge att bryta med GCC! Om du lägger till-Wsuggest-overrideså här kommer ditt bygge att bryta med-Felpå Clang 10! Fråga dig själv:vill du verkligen spåra kompatibilitet med varningsflaggor mellan kompilatorleverantörer och versioner? Är det en bra användning av din tid?

Jag är här för att berätta detdu vill inte, och detdet är slöseri med tid. Du kan spara dig själv mycket krångel om duinkludera endast krav för fast konstruktion i CMakeLists.txt. Din kodkommerbygga utan några varningar aktiverade, så de hör inte hemma där. Tidigare skulle du ha behövt skapa en verktygskedja eller åtminstone skydda dessa inställningar med lämpliga kontroller ochalternativ()för att inaktivera dem. Men sedan CMake 3.19 kan du lägga till dessa i enförinställa. Skapa en fil med namnetCMakePresets.jsonbredvid dinCMakeLists.txtmed detta innehåll:

{ "version": 1, "cmakeMinimumRequired": { "större": 3, "mindre": 19, "lappa": 0 }, "konfigurera förinställningar": [ { "namn": "gcc", "displayName": "GCC", "beskrivning": "Standard byggalternativ för GCC", "generator": "Ninja", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_CXX_FLAGS": "-Wsuggest-override" } }, { "namn": "klang", "displayName": "Klang", "beskrivning": "Standard byggalternativ för Clang", "generator": "Ninja", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_CXX_FLAGS": "-Winconsistent-missing-override -Winconsistent-missing-destructor-override" } } ]}

Sedan någon (en slu*tanvändare, CI,du) kan använda din förinställning så här:

$cmmake --förinställa=gcc -DCMAKE_BUILD_TYPE=Släpp$cmmake --bygga bygga

Förinställningar kommerförändras i grundenhur människor arbetar med CMake och delar sina valfria (men önskade) bygginställningar med användarna. De minskar också avsevärt risken för att ditt bygge går sönder med en annan kompilator eller version.Kom ihåg:det är mycket lättare att skriva en korrekt build genom att behålla din CMakeLists.txtminimaloch skriva en opt-in förinställning än genom att kontrollera alla relevanta faktorer (kompilatorleverantör, version, aktivt språk, etc.) innan du lägger till en flagga.

För att verkligen köra denna punkt hem, lägger den här koden säkert till-Wsuggest-override. Det borde bränna dina ögonglober:

# Mina ögon! Glasögonen gör ingenting!alternativ(MyProj_ENABLE_WARNINGS "Kompilera MyProj med varningar som används av uppströms" AV)om (MyProj_ENABLE_WARNINGS) # håll linjebredden låg uppsättning(is_clang "$") uppsättning(is_gcc "$") uppsättning(ver "$") target_compile_options( mål PRIVAT "$<$>:-Wsuggest-override>" "$<$>:-Wsuggest-override>" )endif ()

Sådant här skalar inte. Om en förinställning inte fungerar för en slu*tanvändare kan de åsidosätta den bitvis på kommandoraden. Å andra sidan, felaktig CMake-kodorsakar ett felutan någon utväg än att lappa ditt bygge.

Glöm inte att andra människor förutom ditt kärnutvecklingsteam kommer att använda ditt bygge. Paketunderhållare, konsumenter av ditt bibliotek (om tillämpligt) och avancerade användare som vill vara i framkant kommer alla att vilja bygga ditt paket med en något annorlunda uppsättning flaggor, kompilatorer, versioner och operativsystem. Vägen för minsta motstånd (med förinställningar) gör båda din CMakeLists.txt lätt att underhållatill digoch lätt att konsumera förAlltdina användare.

Fallgrop: dålig beroendehantering

Om du bara använder väluppfostrade CMake-paket medhitta_paket, detta kommer till stor del att sköta sig själv. Tyvärr är många CMake-paket inte väluppfostrade. För att hålla den här artikeln fokuserad kommer strategier för att hantera dåliga CMake (och icke-CMake) beroenden att behandlas i del 3.

Räkna med att inkrementella byggnader fungerar

Vissa särskilt patologiska projekt kräver att du kör CMakedubbeltfram för att få en korrekt konstruktion. Detta bör aldrig vara fallet och omfattas av en-konfigureringsreceptet ovan. Det är också ganska ovanligt.

Men besvikelse många projekt kräver att du manuellt kör om CMake innan någon inkrementell konstruktion. Hela poängen med CMake är att skapa trogna implementeringar av den abstrakta byggmodellen. Ett konfigureringssteg borde vara allt du behöver. Efter den första körningen kan byggverktyget (t.ex.göra) bör veta när den behöver köra om CMake.

Den tekniska termen här är"idempotens": att köra CMake-konfigurationssteget två gånger med samma ingångar bör inte skilja sig från att köra det en gång. Allt annat beteende är ovänligt för utvecklare och bör betraktas som en bugg med projektet.(Obs: Xcode har några arkitektoniska begränsningar som gör detta omöjligt; sedenna diskursdiskussionför mer detaljer)

Fallgrop: skrämmande cachebeteende

Det finns flera sätt att oavsiktligt bryta idempotens. Om du använderset(CACHE), det finns en god chans att din konstruktion är trasig. Här är ett exempel från abuggrapportJag ansökte nyligen. Om du undrade vad den "plågsamma smärtan" jag har pratat om är, leta inte längre. Det här är sådant som ingen någonsin borde behöva veta från början. Anta att du har följande:

cmake_minimum_required(VERSION 3.20)projekt(testa SPRÅK INGEN)uppsättning(var 1)uppsättning(var 2 CACHE STRÄNG "")meddelande(STATUS "var = ${var}")

Vad skriver den ut? Låt oss se:

$cmmake -S . -B bygga-- var = 2

Vad hände här? Ta verkligen en minut att tänka på vad den underliggande regeln kan vara. Låt oss nu försöka köra samma kommando igen, utan att ändra absolut någonting:

$cmmake -S . -B bygga-- var = 1

Vad du än trodde att regeln var, jag slår vad om att du inte förväntade dig detta. Varför är det1, nu?

Jag fyller i dig: när CMake körs laddar den cachen till ett speciellt globalt omfång. Närset(CACHE)körs, kontrollerar den om det redan finns en post i cachen. Om inte, skapar den en och tar bort den normala variabelbindningen för att exponera det nyligen cachade värdet. Annars kommer det inte att göra någonting alls (om inteTVINGAär specificerad). Fråga mig inte hur det fungerar om det finns flera variabler med samma namn i kapslade kataloger eller funktionsomfång. Jag är inte säker på att jag ens vill veta.

Låt oss nu försöka ställa in cachevariabeln på kommandoraden:

$cmmake -S . -B bygga -Dörr=3-- var = 3

Vad hände här?! Inget av värdet spelade någon roll! Normalvariabeln vann tidigare, men nuset(CACHE)skrivit över det? Varför? Har kommandoradsvariabler sin egen, speciella, innersta omfattning? Är de oföränderliga?

Tja, här är svaret: ställa in en cachevariabel på kommandoraden utan typraderarden typ som redan var etablerad (vad?), och såset(CACHE)kommer att lägga till typen när den körs (ok...), och när detta händer kommer den också att ta bort den normala bindningen som om variabeln inte existerade alls (vad?!), och det är inte ens dokumenterat beteende (VAD) ?!). Om du använder-Dvar:STRING=3istället skrivs den ut1.

Här är vaddokumenten domåste säga om detta:

Om cache-posten inte finns före samtaletellerdeTVINGAalternativ gesdå kommer cache-posten att ställas in på det angivna värdet. Dessutom kommer all normal variabelbindning i det aktuella omfånget att tas bort för att exponera det nyligen cachade värdet för eventuella omedelbart efterföljande utvärderingar.

Det är möjligt för cache-posten att existera före anropet men har ingen typ inställd om den skapades på cmake(1) kommandoraden av en användare via-D==utan att ange en typ. I det här fallet lägger kommandot set till typen.

Ingenstans i den sista meningen står det att den kommer att ta bort den normala variabelbindningen när typen inte är inställd. Hela detta beteende är rent bysantinskt.

Tack och lov har utvecklarna implementerat en policyfix som kommer att skickas i CMake 3.21! MedCMP0126aktiverat, denuppsättningkommandot kommer inte att röra normala variabler, vilket betyder att de alltid "vinner". Detta är huralternativ()fungerar och är ännu en anledning att använda den senaste CMake. Tills dess tror jag att bästa praxis är att endast cachelagra en befintlig normal variabel, skyddad av en kontroll om den redan finns:

om (INTE DEFINIERAD var) # beräkna standardvärde för var uppsättning(var "${was}" CACHE  "doc string")endif ()

Detta säkerställer att värdet avvarär konsekvent oavsett tillståndet för cachen. Efter CMake 3.21 kan du säkert ställa in cachevariabeln direkt och till valfritt standardvärde.

Fallgrop: konfigureringsstegsberoenden

En annan vanlig orsak till byggproblem är att misslyckas med att deklarera ett beroende för konfigureringssteget. Om ditt projekt har stor användning avexekvera_processeller på annat sätt läser och skriver filer under konfigureringssteget, bör dessa filer läggas till iCMAKE_CONFIGURE_DEPENDSkatalogegenskap, som så:

# både `.` och `fil` är relativa till aktuell källkatalogset_property(KATALOG . BIFOGA FAST EGENDOM CMAKE_CONFIGURE_DEPENDS "fil")

Detta kommer att få den genererade builden att kontrollera dessa filer och köra CMake igen om de har ändrats. Vissa kommandon, somkonfigurera_fil, är smarta nog att uppdatera den här egenskapen automatiskt. Andra, gillarfil (COPY)är inte; använda sig avkonfigurera_filtill förmån för andra "motsvarande" kommandon när du kan. Kontrollera dokumentationen (eller ännu bättre, skriv ett testfall) om du någonsin är osäker.

Fallgrop: fil globbing

Samma problem påverkar också globbing för källfiler i CMake:

# VARNING: den här koden bryter idempotensfil(GLOB källor "*.cpp")add_executable(min_app ${källor})

Om du har den här koden lägger du till en ny.cppfilen till katalogen kommer inte att utlösa en omkonfigurering i en inkrementell build. Som vi diskuterade ovan är detta dåligt beteende eftersom det tvingar en utvecklare att köra om CMake i motsats till bara byggverktyget.

En lösning är att användaCONFIGURE_DEPENDS, vilket kommer att få den genererade builden att omvärdera globs och omkonfigurera om något ändras. Denna kod ställer in beroenden korrekt.

# Den här koden är bra, men med varningar.fil(GLOB källor CONFIGURE_DEPENDS "*.cpp")add_executable(min_app ${källor})

Utvecklarna lovar dock inte att det kommer att fungera på varje generator. Här är vaddokumentationsäger:

Notera:Vi rekommenderar inte att du använder GLOB för att samla in en lista med källfiler från ditt källträd. Om ingen CMakeLists.txt-fil ändras när en källa läggs till eller tas bort kan det genererade byggsystemet inte veta när det ska be CMake att återskapa. DeCONFIGURE_DEPENDSflaggan kanske inte fungerar tillförlitligt på alla generatorer, eller om en ny generator läggs till i framtiden som inte kan stödja den, kommer projekt som använder den att fastna. Även omCONFIGURE_DEPENDSfungerar tillförlitligt, det finns fortfarande en kostnad att utföra kontrollen vid varje ombyggnad.

Detta är inte ett teoretiskt problem: den omåttligt populära Ninja-generatorn har en bugg fram till 1.10.2 (som i skrivande stund är den nyaste).Här är en länktill ett GitHub-nummer om detta.

Jag förstår att detta är kontroversiellt, men med tanke på att CMake-underhållarna är så tydliga när det gäller att inte globba, tror jag starkt att det bästa man kan göra är attlista källfiler uttryckligen. I allmänhet är det en bra idé att undvika att göra saker som uttryckligen inte stöds eftersom när du stöter på problem kommer underhållarna helt enkelt att säga åt dig att fixa din kod.

Dessutom är manuell listning av källfiler vanligtvis bara irriterande i början av ett nytt projekt, när kodstrukturen är mycket mer flytande. I det stationära tillståndet ändras fillistor endast ibland, och smärtan med att uppdatera en fillista är inte särskilt stor. Du kan (och bör) vanligtvis dela upp dina fillistor med hjälp avtarget_sourcesochadd_subdirectory. På det sättet ingenCMakeLists.txtblir för lång.

Uppdatering: en notering om prestanda

En tidigare version av den här artikeln upprepade den gamla sågen att globs är långsamma. Som svar på diskussionen på Reddit körde jag några tester själv och fick en blandad sak. Här är en tabell över mina resultat:

Disk Filsystem OS Generator N Tid(er)
Samsung SSD 970 EVO ext4 (WSL) Ubuntu 20.04 (WSL) Ninja 1000 0,0069
SanDisk SDSSDHII ext4 Ubuntu 20.04 Ninja 1000 0,0162
SanDisk SDSSDHII NTFS Windows 10 Ninja 1000 0,0364
Samsung SSD 970 EVO ext4 (WSL) Ubuntu 20.04 (WSL) Ninja 10 000 0,0481
SanDisk SDSSDHII ext4 Ubuntu 20.04 Ninja 10 000 0,0594
SanDisk SDSSDHII NTFS Windows 10 VS 2019 1000 0,0731
Samsung SSD 970 EVO NTFS Windows 10 Ninja 1000 0,0832
Samsung SSD 970 EVO NTFS Windows 10 VS 2019 1000 0,1012
Samsung SSD 970 EVO NTFS (3g) Ubuntu 20.04 Ninja 1000 0,1146
SanDisk SDSSDHII NTFS (3g) Ubuntu 20.04 Ninja 1000 0,1170
SanDisk SDSSDHII NTFS (9p) Ubuntu 20.04 (WSL) Ninja 100 0,2062
Samsung SSD 970 EVO NTFS (9p) Ubuntu 20.04 (WSL) Ninja 100 0,2268
SanDisk SDSSDHII NTFS Windows 10 Ninja 10 000 0,2743
Samsung SSD 970 EVO ext4 (WSL) Ubuntu 20.04 (WSL) Ninja 100 000 0,3712
SanDisk SDSSDHII ext4 Ubuntu 20.04 Ninja 100 000 0,4383
SanDisk SDSSDHII NTFS Windows 10 VS 2019 10 000 0,4710
Samsung SSD 970 EVO NTFS Windows 10 Ninja 10 000 0,5616
Samsung SSD 970 EVO NTFS Windows 10 VS 2019 10 000 0,8158
SanDisk SDSSDHII NTFS (3g) Ubuntu 20.04 Ninja 10 000 1,1119
Samsung SSD 970 EVO NTFS (3g) Ubuntu 20.04 Ninja 10 000 1,4825
SanDisk SDSSDHII NTFS (9p) Ubuntu 20.04 (WSL) Ninja 1000 1,9585
Samsung SSD 970 EVO NTFS (9p) Ubuntu 20.04 (WSL) Ninja 1000 2,1879

Från mina tester verkar det som att ext4 är ett anmärkningsvärt motståndskraftigt filsystem. Jag tror att det inte finns något prestationsargument mot globbing på ext4. Det är också ganska tydligt att du inte ska använda ntfs-3g, eller speciellt WSL2 NTFS 9p FUSE-drivrutinerna. Bygg på ext4 och kopiera utdata till en NTFS-volym om det behövs. VS 2019 är långsammare än Ninja, men även vid 10 000 filer tog det under en sekund att skanna 10 000 källor, så det här är förmodligen inte ett problem i absoluta termer.

Av någon konstig anledning var NTFS långsammare på min NVMe-enhet än på min SATA-enhet. Jag testade båda enheterna medwinsat disk-enhet X, och det visade att min NVMe-enhet är betydligt snabbare. Kanske finns det några förarkonstigheter här eftersom det snabbaste resultatet för N=1000 var (virtualiserat!) ext4 på den enheten.

Jag har publicerat Python-skriptet som jag använde för att testa dettahär. Det finns ett GitHub Actions-arbetsflöde som kör skriptet på Windows, macOS och Linux för N=1000. Jag förväntade mig att de virtualiserade diskarna på GitHub Actions skulle vara långsamma, men de var faktiskt mycket snabba, med resultat som liknar det jag rapporterade ovan.

Jag är nyfiken på att höra rapporter från läsare och frånMesonochNinjautvecklare för att se om de har mer information om varför globs är för långsamma för sina system.

Räkna med att standard CMake-variabler respekteras

Ett stort antal variabler i CMake är designade att ställas in externt. Den kanske mest kända av dessa ärCMAKE_CXX_FLAGSoch dess konfigurationsspecifika varianterCMAKE_CXX_FLAGS_DEBUG,CMAKE_CXX_FLAGS_RELEASE, etc.Rör inte dessa variabler!

Som en baslinje, rör intenågrastandardvariabler om de redan är definierade när din build körs. Flytta dina föredragna standardinställningar till förinställningar eller använd teknikerna ovan för att uppdatera cachen på ett säkert sätt. På äldre CMake-versioner kan de ställas in i en verktygskedja som ett alternativ till förinställningar. En fullständig lista över variabler finns idokumentationen, men de flesta börjar medCMAKE_. Anmärkningsvärda undantag inkluderarBUILD_SHARED_LIBSoch_ROOT.

I många fall finns det bättre sätt att ställa in ett byggkrav än genom att klippa en reserverad variabel. Till exempel, om du vill ställa in C++-versionen bör du användamålfunktioner, snarare än inställningCMAKE_CXX_STANDARDeller(flämtning!)redigeringCMAKE_CXX_FLAGS.

target_compile_features(min_exe PRIVAT cxx_std_14)target_compile_features(my_lib OFFENTLIG cxx_std_17) # PUBLIC så att länkade använder >= C++17

Att ställa standardkravet som enOFFENTLIG(verkligenGRÄNSSNITT) egendom på ett bibliotek kommer att sprida detta till länkade även efter exportmy_libför användning i enhitta_paketmodul. Vi pratar mer om förpackning och att vara ett bra beroende om några veckor.

Vissa bibliotek (somfirning) ändra deras ABI beroende på den aktiva standardversionen. Om du måste göra detta kan du koda kravet genom att markeraCMAKE_CXX_STANDARDatt välja rättcxx_std_Nfunktion för att fungera som ett användningskrav:

# C++14 eller högre krävs för my_libom (CMAKE_CXX_STANDARD STÖRRE 14) target_compile_features(my_lib OFFENTLIG cxx_std_${CMAKE_CXX_STANDARD})annan () target_compile_features(my_lib OFFENTLIG cxx_std_14)endif ()

Hur som helst kan dina användare ställa in en högreCMAKE_CXX_STANDARDvärde på kommandoraden. Detta ger dina användare möjlighet att säkerställa ABI-kompatibilitet när de använder experimentellt stöd för utkast till C++-standarder när de bygger från källan. Om du ställer inCMAKE_CXX_STANDARDvillkorslöst tar du denna kontroll från dina användare.

slu*tsats

Det här är vad du bör ta med dig från det här inlägget:

  1. DinCMakeLists.txtfilen ska varaminimaloch inkluderar endast krav på fast konstruktion; allt annat bör vara opt-in (helst i en förinställning). Varningsflaggor är inga fasta krav.
  2. Konfigurationssteget för din build ska aldrig behöva köras två gånger i rad med samma inställningar, och inkrementella builds bör inte kräva att användaren manuellt kör om CMake. Detta innebär att användaCONFIGURE_DEPENDSpå globs eller ännu bättre, undvika dem.
  3. Var försiktig när du ställer in en cachevariabel, även utanTVINGA, eftersom det kan ta bort en normal variabel på ett oförutsägbart sätt. Före CMake 3.21 (ej släppt), gör det inteset(CACHE)utan att bekräfta att variabeln inte existerar.
  4. Undvik att röra standard CMake-variabler; föredramålegenskapereller flytta sådana inställningar till förinställningarna (åtminstone gör dina redigeringar opt-in på något sätt). slu*ta tänka i termer avflaggoroch börja tänka i termer avmål. Det är mycket vanligt att nybörjare (eller till och med skickliga) CMake-programmerare arbetar sig in i enXY problemoch försök att skohorn i en kompilatorspecifik miljö som redan har abstraherats.

Nästa gång ska vi prata om målmodellen och hur man hanterar beroenden i modern CMake. Tills dess, gå med i konversationenhär på Reddit!

Hur man använder CMake utan den plågsamma smärtan - Del 2 (2024)
Top Articles
Latest Posts
Article information

Author: Lakeisha Bayer VM

Last Updated:

Views: 5920

Rating: 4.9 / 5 (49 voted)

Reviews: 80% of readers found this page helpful

Author information

Name: Lakeisha Bayer VM

Birthday: 1997-10-17

Address: Suite 835 34136 Adrian Mountains, Floydton, UT 81036

Phone: +3571527672278

Job: Manufacturing Agent

Hobby: Skimboarding, Photography, Roller skating, Knife making, Paintball, Embroidery, Gunsmithing

Introduction: My name is Lakeisha Bayer VM, I am a brainy, kind, enchanting, healthy, lovely, clean, witty person who loves writing and wants to share my knowledge and understanding with you.